Repository navigation
fix(http2): preserve the complete response on RST_STREAM(NO_ERROR) - #2012
Conversation
8a90048 to
c4f8ca5
Compare
c4f8ca5 to
d538899
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 |
> [!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
…into mosum/http2-3b-preserve-response-on-rst-no-error # 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
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: