Skip to content

nghttp3: a late answer to a cancelled request is dropped instead of closing the connection - #291

Merged
MDA2AV merged 1 commit into
mainfrom
fix/nghttp3-cancelled-response
Oct 12, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/nghttp3-cancelled-response

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 7, 2026

Copy link
Copy Markdown
Owner

An nghttp3 handler that was still running when the client cancelled its request closed the whole connection when it answered.

A client that gives up on a request sends STOP_SENDING and RESET_STREAM, as a browser does when a tab closes. nghttp3 then drops the stream. The handler, buffered or streaming, still finishes and submits its answer, and ih3_submit_response returns ERR_STREAM_NOT_FOUND. QueueResponse treated that as a protocol failure: the connection closed, taking every other request on it down with the one the client abandoned. The pure C# stack (ioxide.http3) already handles this.

QueueResponse now drops an answer whose stream nghttp3 no longer knows, the same way the transport already treats ngtcp2's STREAM_NOT_FOUND on a send. That covers both completion paths and FailStreamed's 500, with no new state. nghttp3 copies the headers and body at submit, so nothing is left behind.

Tests

New, in H3Tests:

  • h3: a late buffered answer to a cancelled request leaves the connection serving
  • h3: a late streaming answer to a cancelled request leaves the connection serving
  • control: a late answer to a cancelled request leaves an ioxide.http3 connection serving

The handler waits on a RingTimer and answers only after the server has seen the stream close (a probe overrides OnStreamClosed). The tests assert that it did, so they can't pass on an answer that wasn't actually late. The client then sends another request on the same connection.

main this PR
next request after a late answer, nghttp3 buffered and streaming -1, connection closed (5 of 5) 200
ioxide.http3 control 200 200

All suites pass: E2E 255, Unit 63.

…losing the connection

An async handler, buffered or streaming, that was still running when the client cancelled its
request (STOP_SENDING and RESET_STREAM, as a closed tab sends) submitted its answer after the stream
had closed. nghttp3 had already dropped the stream and returned ERR_STREAM_NOT_FOUND, and
QueueResponse treated that as a protocol failure: the next packet ended the run loop and closed the
connection, taking every other request on it down with the one the client gave up on.

QueueResponse now drops an answer whose stream nghttp3 no longer knows, as the transport already
does with ngtcp2's STREAM_NOT_FOUND on a send. That covers both completion paths and FailStreamed's
500, with no new state.

Covered by "h3: a late buffered answer to a cancelled request leaves the connection serving", its
streaming twin, and an ioxide.http3 control next to them in H3Tests. The handler answers only once a
probe connection has seen its stream close on the server, and the tests assert that it did.
Confirmed both ways: on the unmodified source both nghttp3 tests failed in 5 of 5 runs
("submit_response failed: ERR_STREAM_NOT_FOUND", the next request got -1 and the server closed the
connection) while the control passed; with the fix all three pass.
@MDA2AV
MDA2AV merged commit 4b29260 into main Oct 12, 2026
1 check passed
@MDA2AV MDA2AV mentioned this pull request Oct 12, 2026
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.

1 participant