]> git.kaiwu.me - njs.git/commitdiff
Fetch: avoid reusing framed response connections
authorDmitry Volyntsev <xeioex@nginx.com>
Wed, 16 Sep 2026 00:42:55 +0000 (17:42 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 16 Sep 2026 21:17:38 +0000 (14:17 -0700)
Previously, fetch cached fixed-length responses with bytes after their
declared body. Bodyless and chunked responses with such bytes did not
complete correctly.

The fix is to close a framed response connection with unconsumed bytes
instead of adding it to the keepalive cache.

nginx/ngx_js_http.c
nginx/t/js_fetch_keepalive.t

index 43984c1e87a4d49ed8ebd35be4771b46ae48a9e4..4d88fe2293c909d06aa6d07234c68fba9355e70d 100644 (file)
@@ -1028,6 +1028,10 @@ ngx_js_http_process_body(ngx_js_http_t *http)
     b = http->buffer;
 
     if (http->body == NGX_JS_HTTP_BODY_NONE) {
+        if (b->pos != b->last) {
+            http->keepalive = 0;
+        }
+
         http->ready_handler(http);
         return NGX_DONE;
     }
@@ -1049,6 +1053,10 @@ ngx_js_http_process_body(ngx_js_http_t *http)
         if (rc == NGX_OK) {
             b->pos = http->http_chunk_parse.pos;
 
+            if (b->pos != b->last) {
+                http->keepalive = 0;
+            }
+
             http->ready_handler(http);
             return NGX_DONE;
         }
@@ -1099,6 +1107,10 @@ ngx_js_http_process_body(ngx_js_http_t *http)
             rc = (need > chsize) ? NGX_AGAIN : NGX_DONE;
 
             if (rc == NGX_DONE) {
+                if (b->pos != b->last) {
+                    http->keepalive = 0;
+                }
+
                 http->ready_handler(http);
                 return NGX_DONE;
             }
index 34b119e17a896f8158b10081ad9cf46180d917a4..cd4aa9be322ea29fa43240713cb24e6baf99759d 100755 (executable)
@@ -164,10 +164,12 @@ $t->write_file('test.js', <<EOF);
     }
 
     async function keepalive_head(r) {
+        const loc = r.args.loc || 'count';
+        const port = r.args.port || $p1;
         let responses = [];
 
         for (let i = 0; i < 3; i++) {
-            let reply = await ngx.fetch('http://127.0.0.1:$p1/count',
+            let reply = await ngx.fetch(`http://127.0.0.1:\${port}/\${loc}`,
                                         {method: 'HEAD'});
             responses.push(reply.headers.get('Connection-ID'));
         }
@@ -183,7 +185,7 @@ $t->try_run('no js_fetch_keepalive');
 $t->run_daemon(\&http_daemon, $p2);
 $t->waitforsocket('127.0.0.1:' . $p2);
 
-$t->plan(17);
+$t->plan(20);
 
 ###############################################################################
 
@@ -230,6 +232,26 @@ like(http_get("/keepalive?path=count&port=$p2&sleep=1"), qr/1,2,3/,
        'normal keepalive');
 like(http_get("/keepalive?path=assumed_keepalive&port=$p2&sleep=1"), qr/4,5,6/,
        'assumed keepalive');
+my $r = http_get("/keepalive?path=trailing&port=$p2");
+my ($connections) = $r =~ /(\d+,\d+,\d+)\s*$/;
+my @connections = split /,/, ($connections // '');
+my %seen;
+$seen{$_} = 1 for @connections;
+is(scalar(keys %seen), 3, 'upstream trailing response data');
+
+$r = http_get("/keepalive_head?loc=trailing_head&port=$p2");
+($connections) = $r =~ /(\d+,\d+,\d+)\s*$/;
+@connections = split /,/, ($connections // '');
+%seen = ();
+$seen{$_} = 1 for @connections;
+is(scalar(keys %seen), 3, 'upstream trailing head response data');
+
+$r = http_get("/keepalive?path=trailing_chunked&port=$p2");
+($connections) = $r =~ /(\d+,\d+,\d+)\s*$/;
+@connections = split /,/, ($connections // '');
+%seen = ();
+$seen{$_} = 1 for @connections;
+is(scalar(keys %seen), 3, 'upstream trailing chunked response data');
 
 ###############################################################################
 
@@ -298,6 +320,29 @@ sub http_daemon {
                                        "HTTP/1.0 200 OK" . CRLF .
                                        "Content-Length: " . length($body) . CRLF . CRLF .
                                        $body;
+
+                       } elsif ($uri eq '/trailing') {
+                               print $client
+                                       "HTTP/1.1 200 OK" . CRLF .
+                                       "Content-Length: " . length($ccount) . CRLF .
+                                       "Connection: keep-alive" . CRLF . CRLF .
+                                       $ccount . "trailing";
+
+                       } elsif ($uri eq '/trailing_head') {
+                               print $client
+                                       "HTTP/1.1 200 OK" . CRLF .
+                                       "Content-Length: " . length($ccount) . CRLF .
+                                       "Connection-ID: $ccount" . CRLF .
+                                       "Connection: keep-alive" . CRLF . CRLF .
+                                       $ccount . "trailing";
+
+                       } elsif ($uri eq '/trailing_chunked') {
+                               print $client
+                                       "HTTP/1.1 200 OK" . CRLF .
+                                       "Transfer-Encoding: chunked" . CRLF .
+                                       "Connection: keep-alive" . CRLF . CRLF .
+                                       length($ccount) . CRLF . $ccount . CRLF .
+                                       "0" . CRLF . CRLF . "trailing";
                        }
                }