diff src/http/v2/ngx_http_v2.c @ 6566:ce94f07d5082

HTTP/2: implemented preread buffer for request body (closes #959). Previously, the stream's window was kept zero in order to prevent a client from sending the request body before it was requested (see 887cca40ba6a for details). Until such initial window was acknowledged all requests with data were rejected (see 0aa07850922f for details). That approach revealed a number of problems: 1. Some clients (notably MS IE/Edge, Safari, iOS applications) show an error or even crash if a stream is rejected; 2. This requires at least one RTT for every request with body before the client receives window update and able to send data. To overcome these problems the new directive "http2_body_preread_size" is introduced. It sets the initial window and configures a special per stream preread buffer that is used to save all incoming data before the body is requested and processed. If the directive's value is lower than the default initial window (65535), as previously, all streams with data will be rejected until the new window is acknowledged. Otherwise, no special processing is used and all requests with data are welcome right from the connection start. The default value is chosen to be 64k, which is bigger than the default initial window. Setting it to zero is fully complaint to the previous behavior.
author Valentin Bartenev <vbart@nginx.com>
date Tue, 24 May 2016 17:37:52 +0300
parents 9070ba416284
children bc6fd7afeed6
line wrap: on
line diff
--- a/src/http/v2/ngx_http_v2.c
+++ b/src/http/v2/ngx_http_v2.c
@@ -48,11 +48,6 @@
 
 #define NGX_HTTP_V2_DEFAULT_FRAME_SIZE           (1 << 14)
 
-#define NGX_HTTP_V2_MAX_WINDOW                   ((1U << 31) - 1)
-#define NGX_HTTP_V2_DEFAULT_WINDOW               65535
-
-#define NGX_HTTP_V2_INITIAL_WINDOW               0
-
 #define NGX_HTTP_V2_ROOT                         (void *) -1
 
 
@@ -879,8 +874,6 @@ ngx_http_v2_state_data(ngx_http_v2_conne
         return ngx_http_v2_state_skip_padded(h2c, pos, end);
     }
 
-    stream->in_closed = h2c->state.flags & NGX_HTTP_V2_END_STREAM_FLAG;
-
     h2c->state.stream = stream;
 
     return ngx_http_v2_state_read_data(h2c, pos, end);
@@ -891,10 +884,12 @@ static u_char *
 ngx_http_v2_state_read_data(ngx_http_v2_connection_t *h2c, u_char *pos,
     u_char *end)
 {
-    size_t                 size;
-    ngx_int_t              rc;
-    ngx_uint_t             last;
-    ngx_http_v2_stream_t  *stream;
+    size_t                   size;
+    ngx_buf_t               *buf;
+    ngx_int_t                rc;
+    ngx_http_request_t      *r;
+    ngx_http_v2_stream_t    *stream;
+    ngx_http_v2_srv_conf_t  *h2scf;
 
     stream = h2c->state.stream;
 
@@ -913,17 +908,42 @@ ngx_http_v2_state_read_data(ngx_http_v2_
 
     if (size >= h2c->state.length) {
         size = h2c->state.length;
-        last = stream->in_closed;
-
-    } else {
-        last = 0;
-    }
-
-    rc = ngx_http_v2_process_request_body(stream->request, pos, size, last);
-
-    if (rc != NGX_OK) {
-        stream->skip_data = 1;
-        ngx_http_finalize_request(stream->request, rc);
+        stream->in_closed  = h2c->state.flags & NGX_HTTP_V2_END_STREAM_FLAG;
+    }
+
+    r = stream->request;
+
+    if (r->request_body) {
+        rc = ngx_http_v2_process_request_body(r, pos, size, stream->in_closed);
+
+        if (rc != NGX_OK) {
+            stream->skip_data = 1;
+            ngx_http_finalize_request(r, rc);
+        }
+
+    } else if (size) {
+        buf = stream->preread;
+
+        if (buf == NULL) {
+            h2scf = ngx_http_get_module_srv_conf(r, ngx_http_v2_module);
+
+            buf = ngx_create_temp_buf(r->pool, h2scf->preread_size);
+            if (buf == NULL) {
+                return ngx_http_v2_connection_error(h2c,
+                                                    NGX_HTTP_V2_INTERNAL_ERROR);
+            }
+
+            stream->preread = buf;
+        }
+
+        if (size > (size_t) (buf->end - buf->last)) {
+            ngx_log_error(NGX_LOG_ALERT, h2c->connection->log, 0,
+                          "http2 preread buffer overflow");
+            return ngx_http_v2_connection_error(h2c,
+                                                NGX_HTTP_V2_INTERNAL_ERROR);
+        }
+
+        buf->last = ngx_cpymem(buf->last, pos, size);
     }
 
     pos += size;
@@ -1058,7 +1078,9 @@ ngx_http_v2_state_headers(ngx_http_v2_co
         goto rst_stream;
     }
 
-    if (!h2c->settings_ack && !(h2c->state.flags & NGX_HTTP_V2_END_STREAM_FLAG))
+    if (!h2c->settings_ack
+        && !(h2c->state.flags & NGX_HTTP_V2_END_STREAM_FLAG)
+        && h2scf->preread_size < NGX_HTTP_V2_DEFAULT_WINDOW)
     {
         ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
                       "client sent stream with data "
@@ -2434,8 +2456,7 @@ ngx_http_v2_send_settings(ngx_http_v2_co
 
         buf->last = ngx_http_v2_write_uint16(buf->last,
                                          NGX_HTTP_V2_INIT_WINDOW_SIZE_SETTING);
-        buf->last = ngx_http_v2_write_uint32(buf->last,
-                                             NGX_HTTP_V2_INITIAL_WINDOW);
+        buf->last = ngx_http_v2_write_uint32(buf->last, h2scf->preread_size);
 
         buf->last = ngx_http_v2_write_uint16(buf->last,
                                            NGX_HTTP_V2_MAX_FRAME_SIZE_SETTING);
@@ -2643,6 +2664,7 @@ ngx_http_v2_create_stream(ngx_http_v2_co
     ngx_http_log_ctx_t        *ctx;
     ngx_http_request_t        *r;
     ngx_http_v2_stream_t      *stream;
+    ngx_http_v2_srv_conf_t    *h2scf;
     ngx_http_core_srv_conf_t  *cscf;
 
     fc = h2c->free_fake_connections;
@@ -2756,8 +2778,10 @@ ngx_http_v2_create_stream(ngx_http_v2_co
     stream->request = r;
     stream->connection = h2c;
 
+    h2scf = ngx_http_get_module_srv_conf(r, ngx_http_v2_module);
+
     stream->send_window = h2c->init_window;
-    stream->recv_window = NGX_HTTP_V2_INITIAL_WINDOW;
+    stream->recv_window = h2scf->preread_size;
 
     h2c->processing++;
 
@@ -3411,7 +3435,11 @@ ngx_http_v2_read_request_body(ngx_http_r
     ngx_http_client_body_handler_pt post_handler)
 {
     off_t                      len;
+    size_t                     size;
+    ngx_buf_t                 *buf;
+    ngx_int_t                  rc;
     ngx_http_v2_stream_t      *stream;
+    ngx_http_v2_srv_conf_t    *h2scf;
     ngx_http_request_body_t   *rb;
     ngx_http_core_loc_conf_t  *clcf;
     ngx_http_v2_connection_t  *h2c;
@@ -3444,24 +3472,34 @@ ngx_http_v2_read_request_body(ngx_http_r
 
     r->request_body = rb;
 
+    h2scf = ngx_http_get_module_srv_conf(r, ngx_http_v2_module);
     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
 
     len = r->headers_in.content_length_n;
 
     if (r->request_body_no_buffering && !stream->in_closed) {
-        r->request_body_in_file_only = 0;
 
         if (len < 0 || len > (off_t) clcf->client_body_buffer_size) {
             len = clcf->client_body_buffer_size;
         }
 
+        /*
+         * We need a room to store data up to the stream's initial window size,
+         * at least until this window will be exhausted.
+         */
+
+        if (len < (off_t) h2scf->preread_size) {
+            len = h2scf->preread_size;
+        }
+
         if (len > NGX_HTTP_V2_MAX_WINDOW) {
             len = NGX_HTTP_V2_MAX_WINDOW;
         }
-    }
-
-    if (len >= 0 && len <= (off_t) clcf->client_body_buffer_size
-        && !r->request_body_in_file_only)
+
+        rb->buf = ngx_create_temp_buf(r->pool, (size_t) len);
+
+    } else if (len >= 0 && len <= (off_t) clcf->client_body_buffer_size
+               && !r->request_body_in_file_only)
     {
         rb->buf = ngx_create_temp_buf(r->pool, (size_t) len);
 
@@ -3478,22 +3516,44 @@ ngx_http_v2_read_request_body(ngx_http_r
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
 
+    buf = stream->preread;
+
     if (stream->in_closed) {
         r->request_body_no_buffering = 0;
+
+        if (buf) {
+            rc = ngx_http_v2_process_request_body(r, buf->pos,
+                                                  buf->last - buf->pos, 1);
+            ngx_pfree(r->pool, buf->start);
+            return rc;
+        }
+
         return ngx_http_v2_process_request_body(r, NULL, 0, 1);
     }
 
-    if (len) {
-        if (r->request_body_no_buffering) {
-            stream->recv_window = (size_t) len;
-
-        } else {
-            stream->no_flow_control = 1;
-            stream->recv_window = NGX_HTTP_V2_MAX_WINDOW;
+    if (buf) {
+        rc = ngx_http_v2_process_request_body(r, buf->pos,
+                                              buf->last - buf->pos, 0);
+
+        ngx_pfree(r->pool, buf->start);
+
+        if (rc != NGX_OK) {
+            stream->skip_data = 1;
+            return rc;
         }
-
-        if (ngx_http_v2_send_window_update(stream->connection, stream->node->id,
-                                           stream->recv_window)
+    }
+
+    if (r->request_body_no_buffering) {
+        size = len - h2scf->preread_size;
+
+    } else {
+        stream->no_flow_control = 1;
+        size = NGX_HTTP_V2_MAX_WINDOW - stream->recv_window;
+    }
+
+    if (size) {
+        if (ngx_http_v2_send_window_update(stream->connection,
+                                           stream->node->id, size)
             == NGX_ERROR)
         {
             stream->skip_data = 1;
@@ -3508,9 +3568,13 @@ ngx_http_v2_read_request_body(ngx_http_r
                 return NGX_HTTP_INTERNAL_SERVER_ERROR;
             }
         }
-    }
-
-    ngx_add_timer(r->connection->read, clcf->client_body_timeout);
+
+        stream->recv_window += size;
+    }
+
+    if (!buf) {
+        ngx_add_timer(r->connection->read, clcf->client_body_timeout);
+    }
 
     r->read_event_handler = ngx_http_v2_read_client_request_body_handler;
     r->write_event_handler = ngx_http_request_empty_handler;
@@ -3529,13 +3593,8 @@ ngx_http_v2_process_request_body(ngx_htt
     ngx_http_request_body_t   *rb;
     ngx_http_core_loc_conf_t  *clcf;
 
+    fc = r->connection;
     rb = r->request_body;
-
-    if (rb == NULL) {
-        return NGX_OK;
-    }
-
-    fc = r->connection;
     buf = rb->buf;
 
     if (size) {
@@ -3789,7 +3848,14 @@ ngx_http_v2_read_unbuffered_request_body
         window -= h2c->state.length;
     }
 
-    if (window == stream->recv_window) {
+    if (window <= stream->recv_window) {
+        if (window < stream->recv_window) {
+            ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
+                          "http2 negative window update");
+            stream->skip_data = 1;
+            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+
         return NGX_AGAIN;
     }