Repository navigation
fix(http2): keep flow-controlled end-stream DATA alive until it is flushed (#1998) - #2007
Conversation
e79d54d to
e3c9026
Compare
e3c9026 to
fd56245
Compare
fd56245 to
6259a19
Compare
6259a19 to
4f56a92
Compare
PR HealthLicense 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 Unused Dependencies ✔️
For details on how to fix these, see dependency_validator. This check can be disabled by tagging the PR with Breaking changes ✔️
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 Coverage ✔️
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 Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with |
…ushed (#1998) > [!NOTE] > This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem. ### Summary Fixes #1998: `sendData(bytes, endStream: true)` with more bytes than the peer's flow-control window stalled forever, because the stream was torn down before the flow-controlled remainder could be sent. Two things went wrong: 1. In `StreamMessageQueueOut.enqueueMessage`, `if (message.endStream) startClosing()` ran *before* `_messages.addLast(message)`. `startClosing()` synchronously calls `onCheckForClose()`, which saw an empty `_messages` and closed the queue while the `DataMessage` was still waiting for `WINDOW_UPDATE` credit. 2. In `StreamHandler._handleEndOfStreamRemote`, when the peer sent `END_STREAM` on a stream that was already `HalfClosedLocal` (the local side had called `sendData(..., endStream: true)`), the stream was removed from `_openStreams` immediately, even if `stream.outgoingQueue` still had `DATA` waiting on credit. Subsequent stream-level `WINDOW_UPDATE` frames from the peer were then ignored (RFC 9113 Section 6.9: a `WINDOW_UPDATE` on a closed stream is discarded), so the remaining data could never be sent. ### Changes - **`lib/src/flowcontrol/stream_queues.dart`**: Call `startClosing()` after `_messages.addLast(message)`, so `onCheckForClose()` only closes the queue once `_messages` has drained. - **`lib/src/streams/stream_handler.dart`**: In `_handleEndOfStreamRemote`, only remove the stream from `_openStreams` if `stream.outgoingQueue.wasClosed`; otherwise `_cleanupClosedStream` removes it once the outgoing queue has drained. ### Test Verification (Fails Before $\rightarrow$ Passes After) Added `send-data-exceeding-window-with-end-stream-completes` in `test/transport_test.dart` (server and client each send 100 000 bytes with `endStream: true` through a 65 535-byte window; the second case has the server respond with `END_STREAM` before the upload has drained). **Before fix:** ```text 00:05 +0 -1: transport-test flow-control send-data-exceeding-window-with-end-stream-completes [E] TimeoutException after 0:00:05.000000: Test timed out after 5 seconds. ``` **After fix:** ```text 00:00 +1: All tests passed! ```
4f56a92 to
7f3979c
Compare
… mosum/http2-3-rst-noerror-and-end-stream-flow-control # 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
Fixes #1998:
sendData(bytes, endStream: true)with more bytes than the peer's flow-control window stalled forever, because the stream was torn down before the flow-controlled remainder could be sent.Two things went wrong:
StreamMessageQueueOut.enqueueMessage,if (message.endStream) startClosing()ran before_messages.addLast(message).startClosing()synchronously callsonCheckForClose(), which saw an empty_messagesand closed the queue while theDataMessagewas still waiting forWINDOW_UPDATEcredit.StreamHandler._handleEndOfStreamRemote, when the peer sentEND_STREAMon a stream that was alreadyHalfClosedLocal(the local side had calledsendData(..., endStream: true)), the stream was removed from_openStreamsimmediately, even ifstream.outgoingQueuestill hadDATAwaiting on credit. Subsequent stream-levelWINDOW_UPDATEframes from the peer were then ignored (RFC 9113 Section 6.9: aWINDOW_UPDATEon a closed stream is discarded), so the remaining data could never be sent.Changes
lib/src/flowcontrol/stream_queues.dart: CallstartClosing()after_messages.addLast(message), soonCheckForClose()only closes the queue once_messageshas drained.lib/src/streams/stream_handler.dart: In_handleEndOfStreamRemote, only remove the stream from_openStreamsifstream.outgoingQueue.wasClosed; otherwise_cleanupClosedStreamremoves it once the outgoing queue has drained.Test Verification (Fails Before$\rightarrow$ Passes After)
Added
send-data-exceeding-window-with-end-stream-completesintest/transport_test.dart(server and client each send 100 000 bytes withendStream: truethrough a 65 535-byte window; the second case has the server respond withEND_STREAMbefore the upload has drained).Before fix:
After fix: