[PATCH 1 of 2] HTTP: add support for trailers in HTTP responses

Piotr Sikora piotrsikora at google.com
Sun Jun 26 23:13:15 UTC 2016


# HG changeset patch
# User Piotr Sikora <piotrsikora at google.com>
# Date 1466735480 25200
#      Thu Jun 23 19:31:20 2016 -0700
# Node ID a96187a9806536ab2a8bf3fa7f7b659cf5d3ff13
# Parent  6f69e3c0f780e29bca752fc1f938f4a459a1ec59
HTTP: add support for trailers in HTTP responses.

Example:

   ngx_table_elt_t  *h;

   h = ngx_list_push(&r->headers_out.trailers);
   if (h == NULL) {
       return NGX_ERROR;
   }

   ngx_str_set(&h->key, "Fun");
   ngx_str_set(&h->value, "with trailers");
   h->hash = ngx_hash_key_lc(h->key.data, h->key.len);

The code above adds "Fun: with trailers" trailer to the response to
the request with "TE: trailers" header (which indicates support for
trailers).

Note that trailers MUST be added before last buffer of the response
(last_buf = 1) reaches chunked or v2 output filters, otherwise they
are going to be ignored.

Signed-off-by: Piotr Sikora <piotrsikora at google.com>

diff -r 6f69e3c0f780 -r a96187a98065 src/http/modules/ngx_http_chunked_filter_module.c
--- a/src/http/modules/ngx_http_chunked_filter_module.c
+++ b/src/http/modules/ngx_http_chunked_filter_module.c
@@ -17,6 +17,7 @@ typedef struct {
 
 
 static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf);
+static ngx_chain_t *ngx_http_chunked_get_trailers(ngx_http_request_t *r);
 
 
 static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = {
@@ -69,28 +70,31 @@ ngx_http_chunked_header_filter(ngx_http_
         return ngx_http_next_header_filter(r);
     }
 
-    if (r->headers_out.content_length_n == -1) {
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    if (clcf->chunked_transfer_encoding && r->trailers_ok) {
+        ngx_http_clear_content_length(r);
+        r->chunked = 1;
+
+    } else if (r->headers_out.content_length_n == -1) {
         if (r->http_version < NGX_HTTP_VERSION_11) {
             r->keepalive = 0;
 
+        } else if (clcf->chunked_transfer_encoding) {
+            r->chunked = 1;
+
         } else {
-            clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+            r->keepalive = 0;
+        }
+    }
 
-            if (clcf->chunked_transfer_encoding) {
-                r->chunked = 1;
+    if (r->chunked) {
+        ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_chunked_filter_ctx_t));
+        if (ctx == NULL) {
+            return NGX_ERROR;
+        }
 
-                ctx = ngx_pcalloc(r->pool,
-                                  sizeof(ngx_http_chunked_filter_ctx_t));
-                if (ctx == NULL) {
-                    return NGX_ERROR;
-                }
-
-                ngx_http_set_ctx(r, ctx, ngx_http_chunked_filter_module);
-
-            } else {
-                r->keepalive = 0;
-            }
-        }
+        ngx_http_set_ctx(r, ctx, ngx_http_chunked_filter_module);
     }
 
     return ngx_http_next_header_filter(r);
@@ -201,6 +205,15 @@ ngx_http_chunked_body_filter(ngx_http_re
             b->pos += 2;
         }
 
+        if (r->trailers_ok) {
+            tl->next = ngx_http_chunked_get_trailers(r);
+
+            if (tl->next != NULL) {
+                b->last -= 2;
+                b->last_buf = 0;
+            }
+        }
+
     } else if (size > 0) {
         tl = ngx_chain_get_free_buf(r->pool, &ctx->free);
         if (tl == NULL) {
@@ -230,6 +243,108 @@ ngx_http_chunked_body_filter(ngx_http_re
 }
 
 
+static ngx_chain_t *
+ngx_http_chunked_get_trailers(ngx_http_request_t *r)
+{
+    size_t                          len;
+    ngx_buf_t                      *b;
+    ngx_uint_t                      i;
+    ngx_chain_t                    *cl;
+    ngx_list_part_t                *part;
+    ngx_table_elt_t                *header;
+    ngx_http_chunked_filter_ctx_t  *ctx;
+
+    len = 0;
+
+    part = &r->headers_out.trailers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        len += header[i].key.len + sizeof(": ") - 1 + header[i].value.len
+               + sizeof(CRLF) - 1;
+    }
+
+    if (len == 0) {
+        return NULL;
+    }
+
+    len += sizeof(CRLF) - 1;
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_chunked_filter_module);
+
+    cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    b = cl->buf;
+
+    b->tag = (ngx_buf_tag_t) &ngx_http_chunked_filter_module;
+    b->temporary = 0;
+    b->memory = 1;
+    b->last_buf = 1;
+
+    b->start = ngx_palloc(r->pool, len);
+    if (b->start == NULL) {
+        return NULL;
+    }
+
+    b->end = b->last + len;
+    b->pos = b->start;
+    b->last = b->start;
+
+    part = &r->headers_out.trailers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http trailer: \"%V: %V\"",
+                       &header[i].key, &header[i].value);
+
+        b->last = ngx_copy(b->last, header[i].key.data, header[i].key.len);
+        *b->last++ = ':'; *b->last++ = ' ';
+
+        b->last = ngx_copy(b->last, header[i].value.data, header[i].value.len);
+        *b->last++ = CR; *b->last++ = LF;
+    }
+
+    /* the end of HTTP trailer */
+    *b->last++ = CR; *b->last++ = LF;
+
+    return cl;
+}
+
+
 static ngx_int_t
 ngx_http_chunked_filter_init(ngx_conf_t *cf)
 {
diff -r 6f69e3c0f780 -r a96187a98065 src/http/ngx_http_request.c
--- a/src/http/ngx_http_request.c
+++ b/src/http/ngx_http_request.c
@@ -29,6 +29,8 @@ static ngx_int_t ngx_http_process_connec
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_process_user_agent(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
+static ngx_int_t ngx_http_process_te(ngx_http_request_t *r,
+    ngx_table_elt_t *h, ngx_uint_t offset);
 
 static ngx_int_t ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool,
     ngx_uint_t alloc);
@@ -125,6 +127,10 @@ ngx_http_header_t  ngx_http_headers_in[]
                  offsetof(ngx_http_headers_in_t, if_range),
                  ngx_http_process_unique_header_line },
 
+    { ngx_string("TE"),
+                 offsetof(ngx_http_headers_in_t, te),
+                 ngx_http_process_te },
+
     { ngx_string("Transfer-Encoding"),
                  offsetof(ngx_http_headers_in_t, transfer_encoding),
                  ngx_http_process_header_line },
@@ -559,6 +565,14 @@ ngx_http_create_request(ngx_connection_t
         return NULL;
     }
 
+    if (ngx_list_init(&r->headers_out.trailers, r->pool, 4,
+                      sizeof(ngx_table_elt_t))
+        != NGX_OK)
+    {
+        ngx_destroy_pool(r->pool);
+        return NULL;
+    }
+
     r->ctx = ngx_pcalloc(r->pool, sizeof(void *) * ngx_http_max_module);
     if (r->ctx == NULL) {
         ngx_destroy_pool(r->pool);
@@ -1737,6 +1751,59 @@ ngx_http_process_user_agent(ngx_http_req
 
 
 static ngx_int_t
+ngx_http_process_te(ngx_http_request_t *r, ngx_table_elt_t *h,
+    ngx_uint_t offset)
+{
+    u_char  *p;
+
+    if (ngx_http_process_multi_header_lines(r, h, offset) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (r->http_version < NGX_HTTP_VERSION_11) {
+        return NGX_OK;
+    }
+
+    if (h->value.len == sizeof("trailers") - 1
+        && ngx_memcmp(h->value.data, "trailers", sizeof("trailers") - 1) == 0)
+    {
+        r->trailers_ok = 1;
+        return NGX_OK;
+    }
+
+    if (r->http_version >= NGX_HTTP_VERSION_20) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent HTTP/2 request with invalid value \"%V\" "
+                      "in \"TE\" header", &h->value);
+
+        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+        return NGX_ERROR;
+    }
+
+    if (h->value.len < sizeof("trailers") - 1) {
+        return NGX_OK;
+    }
+
+    p = ngx_strcasestrn(h->value.data, "trailers", sizeof("trailers") - 2);
+    if (p == NULL) {
+        return NGX_OK;
+    }
+
+    if (p == h->value.data || *(p - 1) == ',' || *(p - 1) == ' ') {
+
+        p += sizeof("trailers") - 1;
+
+        if (p == h->value.data + h->value.len || *p == ',' || *p == ' ') {
+            r->trailers_ok = 1;
+            return NGX_OK;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
 ngx_http_process_multi_header_lines(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
diff -r 6f69e3c0f780 -r a96187a98065 src/http/ngx_http_request.h
--- a/src/http/ngx_http_request.h
+++ b/src/http/ngx_http_request.h
@@ -189,6 +189,7 @@ typedef struct {
     ngx_table_elt_t                  *range;
     ngx_table_elt_t                  *if_range;
 
+    ngx_array_t                       te;
     ngx_table_elt_t                  *transfer_encoding;
     ngx_table_elt_t                  *expect;
     ngx_table_elt_t                  *upgrade;
@@ -245,6 +246,7 @@ typedef struct {
 
 typedef struct {
     ngx_list_t                        headers;
+    ngx_list_t                        trailers;
 
     ngx_uint_t                        status;
     ngx_str_t                         status_line;
@@ -514,6 +516,7 @@ struct ngx_http_request_s {
     unsigned                          pipeline:1;
     unsigned                          chunked:1;
     unsigned                          header_only:1;
+    unsigned                          trailers_ok:1;
     unsigned                          keepalive:1;
     unsigned                          lingering_close:1;
     unsigned                          discard_body:1;
diff -r 6f69e3c0f780 -r a96187a98065 src/http/v2/ngx_http_v2_filter_module.c
--- a/src/http/v2/ngx_http_v2_filter_module.c
+++ b/src/http/v2/ngx_http_v2_filter_module.c
@@ -56,7 +56,9 @@ static u_char *ngx_http_v2_string_encode
 static u_char *ngx_http_v2_write_int(u_char *pos, ngx_uint_t prefix,
     ngx_uint_t value);
 static ngx_http_v2_out_frame_t *ngx_http_v2_create_headers_frame(
-    ngx_http_request_t *r, u_char *pos, u_char *end);
+    ngx_http_request_t *r, u_char *pos, u_char *end, ngx_uint_t fin);
+static ngx_http_v2_out_frame_t *ngx_http_v2_create_trailers_frame(
+    ngx_http_request_t *r);
 
 static ngx_chain_t *ngx_http_v2_send_chain(ngx_connection_t *fc,
     ngx_chain_t *in, off_t limit);
@@ -571,7 +573,7 @@ ngx_http_v2_header_filter(ngx_http_reque
                                       header[i].value.len, tmp);
     }
 
-    frame = ngx_http_v2_create_headers_frame(r, start, pos);
+    frame = ngx_http_v2_create_headers_frame(r, start, pos, r->header_only);
     if (frame == NULL) {
         return NGX_ERROR;
     }
@@ -595,6 +597,121 @@ ngx_http_v2_header_filter(ngx_http_reque
 }
 
 
+static ngx_http_v2_out_frame_t *
+ngx_http_v2_create_trailers_frame(ngx_http_request_t *r)
+{
+    u_char           *pos, *start, *tmp;
+    size_t            len, tmp_len;
+    ngx_uint_t        i;
+    ngx_list_part_t  *part;
+    ngx_table_elt_t  *header;
+
+    len = 0;
+    tmp_len = 0;
+
+    part = &r->headers_out.trailers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        if (header[i].key.len > NGX_HTTP_V2_MAX_FIELD) {
+            ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                          "ignoring too long response trailer name: \"%V\"",
+                          &header[i].key);
+            continue;
+        }
+
+        if (header[i].value.len > NGX_HTTP_V2_MAX_FIELD) {
+            ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                          "ignoring too long response trailer value: "
+                          "\"%V: %V\"", &header[i].key, &header[i].value);
+            continue;
+        }
+
+        len += 1 + NGX_HTTP_V2_INT_OCTETS + header[i].key.len
+                 + NGX_HTTP_V2_INT_OCTETS + header[i].value.len;
+
+        if (header[i].key.len > tmp_len) {
+            tmp_len = header[i].key.len;
+        }
+
+        if (header[i].value.len > tmp_len) {
+            tmp_len = header[i].value.len;
+        }
+    }
+
+    if (len == 0) {
+        return NULL;
+    }
+
+    tmp = ngx_palloc(r->pool, tmp_len);
+    pos = ngx_pnalloc(r->pool, len);
+
+    if (pos == NULL || tmp == NULL) {
+        return NULL;
+    }
+
+    start = pos;
+
+    part = &r->headers_out.trailers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0
+            || header[i].key.len > NGX_HTTP_V2_MAX_FIELD
+            || header[i].value.len > NGX_HTTP_V2_MAX_FIELD)
+        {
+            continue;
+        }
+
+#if (NGX_DEBUG)
+        if (r->connection->log->log_level & NGX_LOG_DEBUG_HTTP) {
+            ngx_strlow(tmp, header[i].key.data, header[i].key.len);
+
+            ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                           "http2 output trailer: \"%*s: %V\"",
+                           header[i].key.len, tmp, &header[i].value);
+        }
+#endif
+
+        *pos++ = 0;
+
+        pos = ngx_http_v2_write_name(pos, header[i].key.data,
+                                     header[i].key.len, tmp);
+
+        pos = ngx_http_v2_write_value(pos, header[i].value.data,
+                                      header[i].value.len, tmp);
+    }
+
+    return ngx_http_v2_create_headers_frame(r, start, pos, 1);
+}
+
+
 static u_char *
 ngx_http_v2_string_encode(u_char *dst, u_char *src, size_t len, u_char *tmp,
     ngx_uint_t lower)
@@ -645,7 +762,7 @@ ngx_http_v2_write_int(u_char *pos, ngx_u
 
 static ngx_http_v2_out_frame_t *
 ngx_http_v2_create_headers_frame(ngx_http_request_t *r, u_char *pos,
-    u_char *end)
+    u_char *end, ngx_uint_t fin)
 {
     u_char                    type, flags;
     size_t                    rest, frame_size;
@@ -666,12 +783,12 @@ ngx_http_v2_create_headers_frame(ngx_htt
     frame->stream = stream;
     frame->length = rest;
     frame->blocked = 1;
-    frame->fin = r->header_only;
+    frame->fin = fin;
 
     ll = &frame->first;
 
     type = NGX_HTTP_V2_HEADERS_FRAME;
-    flags = r->header_only ? NGX_HTTP_V2_END_STREAM_FLAG : NGX_HTTP_V2_NO_FLAG;
+    flags = fin ? NGX_HTTP_V2_END_STREAM_FLAG : NGX_HTTP_V2_NO_FLAG;
     frame_size = stream->connection->frame_size;
 
     for ( ;; ) {
@@ -733,7 +850,7 @@ ngx_http_v2_create_headers_frame(ngx_htt
             continue;
         }
 
-        b->last_buf = r->header_only;
+        b->last_buf = fin;
         cl->next = NULL;
         frame->last = cl;
 
@@ -755,7 +872,7 @@ ngx_http_v2_send_chain(ngx_connection_t 
     ngx_http_request_t        *r;
     ngx_http_v2_stream_t      *stream;
     ngx_http_v2_loc_conf_t    *h2lcf;
-    ngx_http_v2_out_frame_t   *frame;
+    ngx_http_v2_out_frame_t   *frame, *trailers;
     ngx_http_v2_connection_t  *h2c;
 
     r = fc->data;
@@ -829,6 +946,8 @@ ngx_http_v2_send_chain(ngx_connection_t 
     frame_size = (h2lcf->chunk_size < h2c->frame_size)
                  ? h2lcf->chunk_size : h2c->frame_size;
 
+    trailers = NULL;
+
 #if (NGX_SUPPRESS_WARN)
     cl = NULL;
 #endif
@@ -891,6 +1010,13 @@ ngx_http_v2_send_chain(ngx_connection_t 
             size -= rest;
         }
 
+        if (cl->buf->last_buf && r->trailers_ok) {
+            trailers = ngx_http_v2_create_trailers_frame(r);
+            if (trailers) {
+                cl->buf->last_buf = 0;
+            }
+        }
+
         frame = ngx_http_v2_filter_get_data_frame(stream, frame_size, out, cl);
         if (frame == NULL) {
             return NGX_CHAIN_ERROR;
@@ -903,6 +1029,11 @@ ngx_http_v2_send_chain(ngx_connection_t 
         stream->send_window -= frame_size;
         stream->queued++;
 
+        if (trailers) {
+            ngx_http_v2_queue_frame(h2c, trailers);
+            stream->queued++;
+        }
+
         if (in == NULL) {
             break;
         }



More information about the nginx-devel mailing list