Skip to content

nghttp2: a response's fields are no longer capped at 64, so one with more no longer ends its connection, and one nghttp2 will not send resets its stream - #515

Open
MDA2AV wants to merge 1 commit into
mainfrom
fix/505-nghttp2-many-response-headers
Open

MDA2AV wants to merge 1 commit into
mainfrom
fix/505-nghttp2-many-response-headers

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

What was wrong

  • The shim packed a response's fields into a fixed nghttp2_nv nv[64], and ih2_unpack_headers returned INVALID_ARGUMENT past it. SubmitResponse and SendStreamedHeaders read any error as a fault in the session and set _failed.
  • So one response with 64 headers of its own, plus :status, ended the connection and every other stream on it. Its own stream got nothing, neither a response nor a reset. nghttp2 has no such limit.
  • Found on the way: nghttp2 drops a response whose header block is past its own 64 KiB limit, with nothing on the wire. The stream stayed open until the peer gave up. Until now the cap hid part of that, but two long values were already enough.

Details are in #505.

The fix

  • The array belongs to the connection now, and grows as needed. ih2_unpack_headers fills it in one pass, nghttp2 copies out of it during the submit, and ih2_free frees it. A response within the first 64 fields costs nothing new but one malloc per connection.
  • A new on_frame_not_send callback resets a stream whose HEADERS nghttp2 would not send (FRAME_SIZE_ERROR) with INTERNAL_ERROR. Only that error: on a stream already gone, a reset would answer the peer's own (RFC 9113 5.4.2).

Native libraries

runtimes/linux-x64 and runtimes/linux-arm64 are rebuilt with scripts/build-nghttp2-native.sh at its pinned nghttp2 ref: x64 on the host, arm64 with the aarch64-linux-gnu cross toolchain in an Ubuntu 24.04 container. Both still need nothing past GLIBC 2.17, and neither build's log has a warning.

#495 and #501 rebuild the same libraries. Whichever merges later has to merge main and rerun the build script, rather than take either side's binaries. The shim's text merges cleanly with both.

Behaviour

  • A response with 64 or more headers now reaches the peer whole. Before, it ended the connection.
  • A response whose header block is past 64 KiB now resets its own stream with INTERNAL_ERROR. A streamed writer sees IsCompleted at its flush. Before, the peer waited on that stream for good.

API

None.

Tests

New in the E2E suite (Nghttp2ManyHeadersTests.cs), through HttpClient over h2c:

  • nghttp2: a buffered response with 63 headers of its own reaches the peer whole, and with 64 and 200
  • nghttp2: a streamed response with 63 headers of its own reaches the peer whole, and with 64 and 200
    • Twice on one connection, so the second reuses what the first grew.

New in the Unit suite (Nghttp2OversizedResponseTests.cs), over the strict test transport:

  • nghttp2: a buffered response with a header block past nghttp2's limit resets its own stream, and the connection serves on
  • nghttp2: a streamed response with a header block past nghttp2's limit hears the peer gone at its flush

Against main's library (the tests only):

FAIL  nghttp2: a buffered response with a header block past nghttp2's limit resets its own stream, and the connection serves on: expected [1 0x2], got []
FAIL  nghttp2: a streamed response with a header block past nghttp2's limit hears the peer gone at its flush: expected [1 0x2], got []
PASS  nghttp2: a buffered response with 63 headers of its own reaches the peer whole
PASS  nghttp2: a streamed response with 63 headers of its own reaches the peer whole
FAIL  nghttp2: a buffered response with 64 headers of its own reaches the peer whole: The response ended prematurely while waiting for the next frame from the server. (ResponseEnded)
FAIL  nghttp2: a streamed response with 64 headers of its own reaches the peer whole: The response ended prematurely while waiting for the next frame from the server. (ResponseEnded)
FAIL  nghttp2: a buffered response with 200 headers of its own reaches the peer whole: The response ended prematurely while waiting for the next frame from the server. (ResponseEnded)
FAIL  nghttp2: a streamed response with 200 headers of its own reaches the peer whole: The response ended prematurely while waiting for the next frame from the server. (ResponseEnded)

With the fix:

  • Unit: 147 passed.
  • E2E: 459 passed.
  • Chaos: 83 passed, including the nghttp2 twins.
  • Http: 95 passed.

Bench

Measured with the same setup as #486:

  • One reactor pinned to CPU 6, and h2load -c 32 -m 16 -t 1 pinned to CPU 2.
  • Main, this branch, and main again from a second worktree as a control.
  • Six interleaved rounds of 10 s; each build is compared with main inside its own round.
Sample Server CPU per request, branch vs main Control vs main
Http2/Nghttp2Buffered -0.1% +1.1%
Http2/Nghttp2Response (8 chunks of 1 KiB) -0.2% +0.9%

Both are inside the noise. A connection gains one malloc of 2.5 KiB, at its first response.

Fixes #505.

…more no longer ends its connection, and one nghttp2 will not send resets its stream

The shim packed fields into a fixed array and returned INVALID_ARGUMENT past it, which the managed side reads as a fault in the session: every other stream on the connection ended, and the response's own got nothing.
The array is the connection's now and grows as needed. Without the cap, a header block past nghttp2's own 64 KiB limit reaches on_frame_not_send instead, which on main left its stream open for good, so that one is reset with INTERNAL_ERROR.

Fixes #505.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nghttp2: a response with 64 headers ends the whole connection, and its own stream gets nothing

1 participant