Repository navigation
fix(http2): preserve the complete response on RST_STREAM(NO_ERROR) - #2012
Conversation
8a90048 to
c4f8ca5
Compare
c4f8ca5 to
d538899
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 |
> [!NOTE] > This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem. ### Summary RFC 9113 Section 8.1: *"A server can send a complete response prior to the client sending an entire request if the response does not depend on any portion of the request that has not been sent and received. When this is true, a server MAY request that the client abort transmission of a request without error by sending a RST_STREAM with an error code of NO_ERROR after sending a complete response."* grpc-go, for example, does exactly this after sending trailers on client-streaming calls. Previously `StreamHandler._handleRstFrame` treated every `RST_STREAM` as abnormal: it called `_closeStreamAbnormally`, which dropped the messages still buffered in `ConnectionMessageQueueIn` / `StreamMessageQueueIn` (anything the application had not consumed yet, e.g. because its listener was paused) and failed the stream with `StreamTransportException (errorCode: 0)` — even though the complete response, including trailers, had already arrived. ### Changes - **`lib/src/flowcontrol/connection_queues.dart`**: Add `ConnectionMessageQueueIn.forceDispatchStreamMessages(streamId)`, which hands the buffered messages of one stream to its `StreamMessageQueueIn` (crediting the connection window for the DATA bytes, as the regular dispatch path does). - **`lib/src/streams/stream_handler.dart`**: In `_handleRstFrame`, when `errorCode == NO_ERROR` and the stream is `HalfClosedRemote` or `Closed` (the peer has already sent `END_STREAM`), flush the buffered messages, close the outgoing side and drop pending outgoing data, and complete the incoming side normally. `onTerminated` is still invoked with `NO_ERROR`. The stream keeps counting as active until the application has drained the preserved messages, exactly like a normally completed stream. ### Test Verification (Fails Before $\rightarrow$ Passes After) Added `rst-stream-no-error-after-end-stream-preserves-messages` in `test/client_test.dart`: the server sends `HEADERS`, `DATA`, trailers with `END_STREAM`, `RST_STREAM(NO_ERROR)` while the client's listener is paused (a `PING`/ACK round trip is used as the barrier instead of a sleep). **Before fix:** ```text 00:00 +0 -1: client-tests client-errors rst-stream-no-error-after-end-stream-preserves-messages [E] Expected: empty Actual: [ StreamTransportException:HTTP/2 error: Stream error: Stream was terminated by peer (errorCode: 0). ] ``` **After fix:** ```text 00:00 +1: All tests passed! ```
d538899 to
12962a5
Compare
Note
This PR was generated by an AI coding agent (Jetski) on behalf of @mosuem.
Summary
RFC 9113 Section 8.1: "A server can send a complete response prior to the client sending an entire request if the response does not depend on any portion of the request that has not been sent and received. When this is true, a server MAY request that the client abort transmission of a request without error by sending a RST_STREAM with an error code of NO_ERROR after sending a complete response." grpc-go, for example, does exactly this after sending trailers on client-streaming calls.
Previously
StreamHandler._handleRstFrametreated everyRST_STREAMas abnormal: it called_closeStreamAbnormally, which dropped the messages still buffered inConnectionMessageQueueIn/StreamMessageQueueIn(anything the application had not consumed yet, e.g. because its listener was paused) and failed the stream withStreamTransportException (errorCode: 0)— even though the complete response, including trailers, had already arrived.Changes
lib/src/flowcontrol/connection_queues.dart: AddConnectionMessageQueueIn.forceDispatchStreamMessages(streamId), which hands the buffered messages of one stream to itsStreamMessageQueueIn(crediting the connection window for the DATA bytes, as the regular dispatch path does).lib/src/streams/stream_handler.dart: In_handleRstFrame, whenerrorCode == NO_ERRORand the stream isHalfClosedRemoteorClosed(the peer has already sentEND_STREAM), flush the buffered messages, close the outgoing side and drop pending outgoing data, and complete the incoming side normally.onTerminatedis still invoked withNO_ERROR. The stream keeps counting as active until the application has drained the preserved messages, exactly like a normally completed stream.Test Verification (Fails Before$\rightarrow$ Passes After)
Added
rst-stream-no-error-after-end-stream-preserves-messagesintest/client_test.dart: the server sendsHEADERS,DATA, trailers withEND_STREAM,RST_STREAM(NO_ERROR)while the client's listener is paused (aPING/ACK round trip is used as the barrier instead of a sleep).Before fix:
After fix: