Repository navigation
fix(http2): ignore PRIORITY and unknown frames on streams and reject non-zero stream control frames - #2006
Conversation
3b76f21 to
10b0ced
Compare
10b0ced to
12f71c3
Compare
12f71c3 to
6c17792
Compare
6c17792 to
8428322
Compare
PR HealthCoverage ✔️
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 License Headers ✔️
All source files should start with a license header. Unrelated files missing license headers
This check can be disabled by tagging the PR with Breaking changes ✔️
This check can be disabled by tagging the PR with Unused Dependencies ✔️
For details on how to fix these, see dependency_validator. This check can be disabled by tagging the PR with 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.
This check can be disabled by tagging the PR with Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with |
…th unconsumed data > [!NOTE] > This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem. ### Summary Per **RFC 9113 Section 5.2 (Flow Control)** and **Section 6.9 (`WINDOW_UPDATE`)**, a receiver that receives a flow-controlled `DATA` frame must always account for its contribution against the connection flow-control window, even when the stream terminates abnormally or its incoming messages are discarded before the application consumes them. Previously: 1. `ConnectionMessageQueueIn.removeStreamMessageQueue(streamId)` discarded buffered `DataMessage`s in `_stream2pendingMessages[streamId]` without calling `_windowUpdateHandler.dataProcessed(ignoredBytes)`. If a stream was paused (or had not yet been listened to) when `DATA` frames arrived and was subsequently terminated (`stream.terminate()` or `RST_STREAM`), those bytes were permanently leaked from the connection-level receive window. Once 65,535 bytes (or `connectionWindowSize`) accumulated across terminated streams, all future streams on the connection deadlocked waiting for connection window credit. 2. `StreamMessageQueueIn.cancel()` only completed `_onCancelCompleter` without draining `_pendingMessages`, updating `bufferIndicator`, or running `onCloseCheck()`. Cancelling `stream.incomingMessages` while paused left `bufferIndicator.wouldBuffer == true`, stranding buffered `DATA` in `ConnectionMessageQueueIn` and keeping the stream's active slot open indefinitely. 3. `StreamHandler._terminateStream` was a no-op when `stream.state == StreamState.Closed`, so calling `stream.terminate()` on a stream whose remote end had already sent `END_STREAM` while the local listener was paused failed to clean up the stream or replenish the connection window. ### Changes - **`lib/src/flowcontrol/connection_queues.dart`**: In `ConnectionMessageQueueIn.removeStreamMessageQueue`, sum the byte lengths of any undelivered `DataMessage`s and call `_windowUpdateHandler.dataProcessed(ignoredBytes)` when `!wasTerminated`. Also terminate any remaining stream message queues in `ConnectionMessageQueueIn.onTerminated`. - **`lib/src/flowcontrol/stream_queues.dart`**: Override `StreamMessageQueueIn.cancel()` to drain `_pendingMessages`, update `bufferIndicator` so `ConnectionMessageQueueIn` flushes any held messages and replenishes the connection window, and run `onCloseCheck()`. - **`lib/src/streams/stream_handler.dart`**: Allow `_terminateStream` to clean up `StreamState.Closed` streams whose `incomingQueue` has not yet closed. ### Test Verification (Fails Before $\rightarrow$ Passes After) Added two regression tests in `test/flow_control_test.dart`, driving the raw peer from #1999: - `terminating a stream with unconsumed buffered DATA replenishes the connection receive window` - `cancelling incomingMessages on a paused stream replenishes the connection window and releases the active stream slot` Wherever a test needs to know that the client has processed the frames sent so far (or finished reacting to a cancel), it uses the peer's PING round trip (`peer.barrier()`) rather than a wall-clock sleep. **Before fix** (tests on `master`): ```text 00:05 +0 -1: terminating a stream with unconsumed buffered DATA replenishes the connection receive window [E] TimeoutException after 0:00:05.000000: Future not completed test/flow_control_test.dart 126:7 _RawPeer.until test/flow_control_test.dart 427:5 main.<fn> 00:10 +0 -2: cancelling incomingMessages on a paused stream replenishes the connection window and releases the active stream slot [E] TimeoutException after 0:00:05.000000: Future not completed test/flow_control_test.dart 126:7 _RawPeer.until test/flow_control_test.dart 469:5 main.<fn> ``` **After fix:** ```text 00:00 +9: All tests passed! ```
…non-zero stream control frames > [!NOTE] > This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem. ### Summary Per **RFC 9113**: - **Section 4.1 (Frame Format)**: *"Implementations MUST ignore and discard frames of unknown types."* - **Section 6.3 (`PRIORITY`)**: *"The `PRIORITY` frame can be sent on a stream in any state, including idle or closed streams."* - **Section 6.5 (`SETTINGS`), Section 6.7 (`PING`), Section 6.8 (`GOAWAY`)**: `SETTINGS`, `PING`, and `GOAWAY` frames apply to the entire connection and MUST be associated with stream `0x0`; receiving one with a non-zero stream identifier MUST be treated as a connection error of type `PROTOCOL_ERROR`. Previously: 1. `Connection._handleFrameImpl` dispatched any frame with `header.streamId != 0` to `_streams.processStreamFrame`, so a `SETTINGS`, `PING`, or `GOAWAY` frame with `streamId != 0` was treated as a stream error (`RST_STREAM(STREAM_CLOSED)`) instead of a connection-level `PROTOCOL_ERROR` (`GOAWAY(PROTOCOL_ERROR)`). 2. `StreamHandler._processStreamFrameInternal` threw `ProtocolException('Unsupported frame type ...')` (terminating the entire connection with `GOAWAY(PROTOCOL_ERROR)`) when a `PriorityFrame` or `UnknownFrame` arrived on an open stream, and threw `StreamClosedException` (sending `RST_STREAM(STREAM_CLOSED)`) when an `UnknownFrame` arrived on an idle or closed stream. Browsers and proxies that send `PRIORITY` frames on open streams therefore lost the whole connection. ### Changes - **`lib/src/connection.dart`**: Reject `SettingsFrame`, `PingFrame`, and `GoawayFrame` with `header.streamId != 0` via `ProtocolException` before stream dispatch. - **`lib/src/streams/stream_handler.dart`**: Ignore `PriorityFrame` and `UnknownFrame` on open streams, and on idle and closed streams (previously only `PriorityFrame` was ignored there). Not changed (possible follow-up): an ignored `PRIORITY`/unknown frame on an idle peer-initiated stream still advances `_highestStreamIdReceived`, which feeds the `last-stream-id` of a later `GOAWAY` (RFC 9113 Section 6.8). That is pre-existing behavior. ### Test Verification (Fails Before $\rightarrow$ Passes After) Added two regression tests in `test/server_test.dart`: - `ignores-priority-and-unknown-frames-on-open-and-idle-streams` - `rejects-control-frames-with-nonzero-stream-id` **Before fix:** ```text 00:00 +0 -1: server-tests normal ignores-priority-and-unknown-frames-on-open-and-idle-streams [E] HTTP/2 error: Connection error: Connection is being forcefully terminated. (errorCode: 1) 00:00 +0 -2: server-tests client-errors rejects-control-frames-with-nonzero-stream-id [E] Expected: <Instance of 'GoawayFrame'> with `errorCode`: <1> Actual: <Instance of 'RstStreamFrame'> Which: is not an instance of 'GoawayFrame' ``` **After fix:** ```text 00:00 +2: All tests passed! ```
8428322 to
2867462
Compare
…ity-unknown-and-control-frames # Conflicts: # pkgs/http2/CHANGELOG.md
Revisions updated by 19 package dependencies found. Moving forward revisions for: http. Commit message: [deps] rev http Revisions updated by `dart tools/rev_sdk_deps.dart http`. http (https://github.com/dart-lang/http/compare/c76dfb3..c76dfb3): c76dfb3 Wed Oct 28 10:21:55 2026 -0700 Brian Quinlan fix(http2): accept larger headers (dart-lang/http#2041) 23495b0 Mon Oct 26 15:17:28 2026 -0700 Brian Quinlan chore(cronet_http): ready 1.10.1 (dart-lang/http#2039) 27591d4 Mon Oct 26 15:08:10 2026 -0700 Brian Quinlan fix(cronet): avoid cronet namespace duplication (dart-lang/http#2034) 9cdb8da Fri Oct 23 11:14:21 2026 -0700 Kevin Moore Tighten pull_request_label workflow permissions (dart-lang/http#2035) 3998b87 Fri Oct 23 07:31:29 2026 +0200 Moritz fix(http2): do not head-of-line block HEADERS, RST_STREAM and GOAWAY behind window-exhausted DATA (dart-lang/http#2013) be2b51f Fri Oct 23 07:31:28 2026 +0200 Moritz fix(http2): Http2Client applies response-body backpressure to the HTTP/2 stream and uses 4 MiB / 16 MiB receive windows (dart-lang/http#2029) 3b504cf Thu Oct 22 06:28:12 2026 +0200 Moritz fix(http2): never write RST_STREAM ahead of the stream's still-queued HEADERS (dart-lang/http#2008) a641fac Thu Oct 22 06:28:12 2026 +0200 Moritz fix(http2): preserve the complete response on RST_STREAM(NO_ERROR) (dart-lang/http#2012) 5d896c7 Thu Oct 22 06:28:11 2026 +0200 Moritz fix(http2): keep flow-controlled end-stream DATA alive until it is flushed (`#1998`) (dart-lang/http#2007) c834aa0 Thu Oct 22 06:28:11 2026 +0200 Moritz fix(http2): ignore PRIORITY and unknown frames on streams and reject non-zero stream control frames (dart-lang/http#2006) 2d35515 Thu Oct 22 06:15:59 2026 +0200 Moritz fix(http2): replenish connection receive window when stream closes with unconsumed data (dart-lang/http#2005) 1217181 Tue Oct 20 19:17:59 2026 -0400 Mouad Debbar [http_multi_server] Prepare to release 3.2.3 (dart-lang/http#2028). http (https://github.com/dart-lang/http/compare/a8a24a0..c76dfb3): c76dfb3 Wed Oct 28 10:21:55 2026 -0700 Brian Quinlan fix(http2): accept larger headers (dart-lang/http#2041) 23495b0 Mon Oct 26 15:17:28 2026 -0700 Brian Quinlan chore(cronet_http): ready 1.10.1 (dart-lang/http#2039) 27591d4 Mon Oct 26 15:08:10 2026 -0700 Brian Quinlan fix(cronet): avoid cronet namespace duplication (dart-lang/http#2034) 9cdb8da Fri Oct 23 11:14:21 2026 -0700 Kevin Moore Tighten pull_request_label workflow permissions (dart-lang/http#2035) 3998b87 Fri Oct 23 07:31:29 2026 +0200 Moritz fix(http2): do not head-of-line block HEADERS, RST_STREAM and GOAWAY behind window-exhausted DATA (dart-lang/http#2013) be2b51f Fri Oct 23 07:31:28 2026 +0200 Moritz fix(http2): Http2Client applies response-body backpressure to the HTTP/2 stream and uses 4 MiB / 16 MiB receive windows (dart-lang/http#2029) 3b504cf Thu Oct 22 06:28:12 2026 +0200 Moritz fix(http2): never write RST_STREAM ahead of the stream's still-queued HEADERS (dart-lang/http#2008) a641fac Thu Oct 22 06:28:12 2026 +0200 Moritz fix(http2): preserve the complete response on RST_STREAM(NO_ERROR) (dart-lang/http#2012) 5d896c7 Thu Oct 22 06:28:11 2026 +0200 Moritz fix(http2): keep flow-controlled end-stream DATA alive until it is flushed () (dart-lang/http#2007) c834aa0 Thu Oct 22 06:28:11 2026 +0200 Moritz fix(http2): ignore PRIORITY and unknown frames on streams and reject non-zero stream control frames (dart-lang/http#2006) 2d35515 Thu Oct 22 06:15:59 2026 +0200 Moritz fix(http2): replenish connection receive window when stream closes with unconsumed data (dart-lang/http#2005) 1217181 Tue Oct 20 19:17:59 2026 -0400 Mouad Debbar [http_multi_server] Prepare to release 3.2.3 (dart-lang/http#2028) Change-Id: I259130f06771f4b7ba61c7610847e6677b5c8ed5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/570641 Reviewed-by: Nate Bosch <[email protected]> Commit-Queue: Brian Quinlan <[email protected]>
Note
This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.
Summary
Per RFC 9113:
PRIORITY): "ThePRIORITYframe can be sent on a stream in any state, including idle or closed streams."SETTINGS), Section 6.7 (PING), Section 6.8 (GOAWAY):SETTINGS,PING, andGOAWAYframes apply to the entire connection and MUST be associated with stream0x0; receiving one with a non-zero stream identifier MUST be treated as a connection error of typePROTOCOL_ERROR.Previously:
Connection._handleFrameImpldispatched any frame withheader.streamId != 0to_streams.processStreamFrame, so aSETTINGS,PING, orGOAWAYframe withstreamId != 0was treated as a stream error (RST_STREAM(STREAM_CLOSED)) instead of a connection-levelPROTOCOL_ERROR(GOAWAY(PROTOCOL_ERROR)).StreamHandler._processStreamFrameInternalthrewProtocolException('Unsupported frame type ...')(terminating the entire connection withGOAWAY(PROTOCOL_ERROR)) when aPriorityFrameorUnknownFramearrived on an open stream, and threwStreamClosedException(sendingRST_STREAM(STREAM_CLOSED)) when anUnknownFramearrived on an idle or closed stream. Browsers and proxies that sendPRIORITYframes on open streams therefore lost the whole connection.Changes
lib/src/connection.dart: RejectSettingsFrame,PingFrame, andGoawayFramewithheader.streamId != 0viaProtocolExceptionbefore stream dispatch.lib/src/streams/stream_handler.dart: IgnorePriorityFrameandUnknownFrameon open streams, and on idle and closed streams (previously onlyPriorityFramewas ignored there).Not changed (possible follow-up): an ignored
PRIORITY/unknown frame on an idle peer-initiated stream still advances_highestStreamIdReceived, which feeds thelast-stream-idof a laterGOAWAY(RFC 9113 Section 6.8). That is pre-existing behavior.Test Verification (Fails Before$\rightarrow$ Passes After)
Added two regression tests in
test/server_test.dart:ignores-priority-and-unknown-frames-on-open-and-idle-streamsrejects-control-frames-with-nonzero-stream-idBefore fix:
After fix: