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 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 |
…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
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: