Repository navigation
Conversation
…leases it, as any other reset does The shim reported a stream's close to the managed side only when it carried an error code, taking a NO_ERROR close for the normal end of a completed stream. A peer's RST_STREAM(NO_ERROR) closes with NO_ERROR too, and on a request still arriving (headers without END_STREAM) that left the half-built request and its arena in _pending until the connection closed (#493). on_frame_recv now reports a peer's RST_STREAM(NO_ERROR) as it arrives, just before nghttp2 closes the stream. on_stream_close cannot tell it from a normal end: nghttp2 marks a stream half-closed when it receives RST_STREAM, before the close callback. Error resets are still reported from on_stream_close, a managed side reading bodies itself still hears every close there, and a normal end still makes no call into C#. Both runtimes' libraries are rebuilt with scripts/build-nghttp2-native.sh; main's shim rebuilds byte for byte identical with the same toolchains (x64 on the host, arm64 cross-compiled in an Ubuntu 24.04 container).
…end by whether our END_STREAM went out From review: the first version reported a peer's RST_STREAM(NO_ERROR) from on_frame_recv, on the premise that on_stream_close could not tell it from a stream's normal end. It can. A normal end always has our END_STREAM out first (nghttp2 sets SHUT_WR when it sends it), and in buffered mode our side cannot finish before the request has, so a NO_ERROR close with SHUT_WR unset is a reset. on_stream_close now reports a NO_ERROR close when nghttp2_session_get_stream_local_close says our side is still open, and on_frame_recv is back as on main. All close reporting stays in one callback, and a stale RST_STREAM on a stream already closed is no longer forwarded. Both runtimes' libraries are rebuilt from this source.
This was referenced Nov 5, 2026
…et at its next flush, with or without bytes staged
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
NO_ERRORclose was taken to be the normal end of a stream whose response had completed.RST_STREAM(NO_ERROR)also closes withNO_ERROR, and it can end a stream whose request is still arriving: headers without END_STREAM, maybe some DATA, then the reset.CallbackStreamErrornever ran. So the half-builtPendingRequest, with its arena holding the headers and any body so far, stayed in_pendinguntil the connection closed.Details are in #493.
The fix
on_stream_closenow also reports aNO_ERRORclose when our side of the stream is still open (nghttp2_session_get_stream_local_closereturns 0).CallbackStreamErrorthen releases the request, as it already did for every other reset.on_stream_close, songhttp2_session_get_stream_remote_closereports 1 for both.manual_flow) still hears every close.Native libraries
runtimes/linux-x64andruntimes/linux-arm64are rebuilt withscripts/build-nghttp2-native.shat its pinned nghttp2 ref:aarch64-linux-gnucross toolchain in an Ubuntu 24.04 container.Both toolchains first rebuilt main's own shim byte for byte identical to the committed libraries, so the only difference in the new ones is this change. Both still need nothing past GLIBC 2.17.
Behaviour
NO_ERRORbefore it ends is now released at once, like any other reset.NO_ERRORafter it was dispatched is marked gone at once (OnPeerReset). Its next flush reportsIsCompleted, with or without bytes staged. Before, a writer reset before its headers went out got no such word: a flush with nothing staged reported success, and one past 64 KiB waited until the connection closed. A reset that arrives in the same read as the request's end comes before the writer exists; that case is nghttp2: a response submitted after the peer reset its stream leaks its body in the shim, and a streamed writer can hang #496.API
None.
Tests
New in the Unit suite (
Nghttp2StreamCloseTests.cs), over the strict test transport:nghttp2: a request its peer resets with NO_ERROR before it ends is releasednghttp2: a request its peer resets with CANCEL before it ends is released, the controlEach sends a POST's headers without END_STREAM, then the reset. It checks that
_pendingis empty (read by reflection, since nothing else can see it), that no handler ran, and that the connection serves its next request.nghttp2: a streamed response its peer resets with NO_ERROR hears the peer gone at a flush of 131072 B, and of 0 BIsCompleted.Against main's library (the tests only):
With the fix:
Bench
The normal end of a stream gains one stream lookup in C (
nghttp2_session_get_stream_local_close). Measured with the same setup as #486:h2load -c 32 -m 16 -t 1pinned to CPU 2.Http2/Nghttp2BufferedHttp2/Nghttp2Response(8 chunks of 1 KiB)Both are inside the noise; the per-round ranges overlap.
Review
A review of the first version found the premise above wrong: it reported the reset from
on_frame_recv, on the grounds thaton_stream_closecould not tell it from a normal end. Checking our own side can, so the report moved back intoon_stream_close. That keeps every close in one callback, and a stale RST_STREAM on a stream already closed is no longer forwarded.Fixes #493.