Skip to content

fix(http2): never write RST_STREAM ahead of the stream's still-queued HEADERS - #2008

Open
mosuem wants to merge 1 commit into
mosum/http2-3b-preserve-response-on-rst-no-errorfrom
mosum/http2-4-control-frame-queue-and-rst-order
Open

mosuem wants to merge 1 commit into
mosum/http2-3b-preserve-response-on-rst-no-errorfrom
mosum/http2-4-control-frame-queue-and-rst-order

Conversation

@mosuem

@mosuem mosuem commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Note

This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.

Summary

StreamHandler._terminateStream (what TransportStream.terminate() ends up in) wrote the RST_STREAM(CANCEL) straight to the FrameWriter, bypassing ConnectionMessageQueueOut. If the stream's opening HEADERS were still sitting in that queue — because the socket was applying backpressure, or because the queue was head-of-line blocked behind flow-controlled DATA of another stream — the peer received RST_STREAM for a stream it had never seen, which is a connection error (RFC 9113 Section 6.4: "RST_STREAM frames MUST NOT be sent for a stream in the "idle" state. If a RST_STREAM frame identifying an idle stream is received, the recipient MUST treat this as a connection error of type PROTOCOL_ERROR"). A client cancelling a request shortly after starting it could thus take down the whole connection.

Changes

  • lib/src/flowcontrol/connection_queues.dart: Add ConnectionMessageQueueOut.cancelStreamMessages(streamId), which drops the stream's queued DataMessages (they would be discarded by the peer anyway) and reports whether a HeadersMessage for the stream is still queued.
  • lib/src/streams/stream_handler.dart: In _terminateStream, enqueue a ResetStreamMessage behind the queued HEADERS when there are any; otherwise write the RST_STREAM directly as before.

Test Verification (Fails Before $\rightarrow$ Passes After)

Added rst-stream-is-sent-after-queued-headers-of-the-stream in test/client_test.dart. The test keeps the client's FrameWriter in its "would buffer" state (the server's StreamIterator pauses the frame stream after each frame, and that pause propagates synchronously into the client's BufferedSink), opens a stream and cancels it immediately, then checks the wire order.

Before fix:

00:00 +0 -1: client-tests client-errors rst-stream-is-sent-after-queued-headers-of-the-stream [E]
  Expected: <Instance of 'HeadersFrame'> with `streamId`: <3>
    Actual: <Instance of 'RstStreamFrame'>
     Which: is not an instance of 'HeadersFrame'

After fix:

00:00 +1: All tests passed!

@mosuem
mosuem added this pull request to stack #2011 October 8, 2026 08:32
@mosuem
mosuem force-pushed the mosum/http2-4-control-frame-queue-and-rst-order branch from 079b3e0 to c720a11 Compare October 8, 2026 10:36
@mosuem mosuem changed the title fix(http2): avoid head-of-line blocking control frames on exhausted connection window and order RST_STREAM after queued HEADERS fix(http2): never write RST_STREAM ahead of the stream's still-queued HEADERS Oct 8, 2026
@mosuem
mosuem removed this pull request from stack #2011 October 8, 2026 10:38
@mosuem
mosuem changed the base branch from mosum/http2-3-rst-noerror-and-end-stream-flow-control to mosum/http2-3b-preserve-response-on-rst-no-error October 8, 2026 10:38
@mosuem
mosuem added this pull request to stack #2016 October 8, 2026 10:42
@mosuem
mosuem force-pushed the mosum/http2-4-control-frame-queue-and-rst-order branch from c720a11 to 34dfcb6 Compare October 8, 2026 12:04
@mosuem
mosuem force-pushed the mosum/http2-4-control-frame-queue-and-rst-order branch from 34dfcb6 to 81d7268 Compare October 9, 2026 07:54
@mosuem
mosuem removed this pull request from stack #2016 October 9, 2026 07:55
@mosuem
mosuem added this pull request to stack #2020 October 9, 2026 07:55
@mosuem
mosuem force-pushed the mosum/http2-4-control-frame-queue-and-rst-order branch from 81d7268 to 262a8a3 Compare October 9, 2026 08:18
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

PR Health

Coverage ✔️
File Coverage
pkgs/http2/lib/src/flowcontrol/connection_queues.dart 💚 94 % ⬆️ 0 %
pkgs/http2/lib/src/streams/stream_handler.dart 💚 91 % ⬆️ 0 %

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

License Headers ✔️
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/http_multi_server/test/cert.dart

This check can be disabled by tagging the PR with skip-license-check.

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?
http2 Non-Breaking 3.1.0 3.2.0-wip 3.2.0-wip ✔️

This check can be disabled by tagging the PR with skip-breaking-check.

Unused Dependencies ✔️
Package Status
http2 ✔️ All dependencies utilized correctly.

For details on how to fix these, see dependency_validator.

This check can be disabled by tagging the PR with skip-unused-dependencies-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

Comment thread pkgs/http2/lib/src/streams/stream_handler.dart
… HEADERS

> [!NOTE]
> This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.

### Summary

`StreamHandler._terminateStream` (what `TransportStream.terminate()` ends up in) wrote the `RST_STREAM(CANCEL)` straight to the `FrameWriter`, bypassing `ConnectionMessageQueueOut`. If the stream's opening `HEADERS` were still sitting in that queue — because the socket was applying backpressure, or because the queue was head-of-line blocked behind flow-controlled `DATA` of another stream — the peer received `RST_STREAM` for a stream it had never seen, which is a connection error (RFC 9113 Section 6.4: *"RST_STREAM frames MUST NOT be sent for a stream in the "idle" state. If a RST_STREAM frame identifying an idle stream is received, the recipient MUST treat this as a connection error of type PROTOCOL_ERROR"*). A client cancelling a request shortly after starting it could thus take down the whole connection.

### Changes
- **`lib/src/flowcontrol/connection_queues.dart`**: Add `ConnectionMessageQueueOut.cancelStreamMessages(streamId)`, which drops the stream's queued `DataMessage`s (they would be discarded by the peer anyway) and reports whether a `HeadersMessage` for the stream is still queued.
- **`lib/src/streams/stream_handler.dart`**: In `_terminateStream`, enqueue a `ResetStreamMessage` behind the queued `HEADERS` when there are any; otherwise write the `RST_STREAM` directly as before.

### Test Verification (Fails Before $\rightarrow$ Passes After)

Added `rst-stream-is-sent-after-queued-headers-of-the-stream` in `test/client_test.dart`. The test keeps the client's `FrameWriter` in its "would buffer" state (the server's `StreamIterator` pauses the frame stream after each frame, and that pause propagates synchronously into the client's `BufferedSink`), opens a stream and cancels it immediately, then checks the wire order.

**Before fix:**
```text
00:00 +0 -1: client-tests client-errors rst-stream-is-sent-after-queued-headers-of-the-stream [E]
  Expected: <Instance of 'HeadersFrame'> with `streamId`: <3>
    Actual: <Instance of 'RstStreamFrame'>
     Which: is not an instance of 'HeadersFrame'
```

**After fix:**
```text
00:00 +1: All tests passed!
```
@mosuem
mosuem force-pushed the mosum/http2-4-control-frame-queue-and-rst-order branch from 262a8a3 to c3e3058 Compare October 9, 2026 08:48
@mosuem
mosuem marked this pull request as ready for review October 9, 2026 08:54
@mosuem
mosuem requested a review from brianquinlan October 9, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant