From 780f43317352be57ea2e7ffd9ad51d53d5b56869 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Tue, 15 Sep 2026 17:42:55 -0700 Subject: [PATCH] Fetch: avoid reusing framed response connections 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 | 12 +++++++++ nginx/t/js_fetch_keepalive.t | 49 ++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c index 43984c1e..4d88fe22 100644 --- a/nginx/ngx_js_http.c +++ b/nginx/ngx_js_http.c @@ -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; } diff --git a/nginx/t/js_fetch_keepalive.t b/nginx/t/js_fetch_keepalive.t index 34b119e1..cd4aa9be 100755 --- a/nginx/t/js_fetch_keepalive.t +++ b/nginx/t/js_fetch_keepalive.t @@ -164,10 +164,12 @@ $t->write_file('test.js', <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"; } } -- 2.47.3