Skip to content

fix(http2): preserve the complete response on RST_STREAM(NO_ERROR) - #2012

Open
mosuem wants to merge 1 commit into
mosum/http2-3-rst-noerror-and-end-stream-flow-controlfrom
mosum/http2-3b-preserve-response-on-rst-no-error
Open

mosuem wants to merge 1 commit into
mosum/http2-3-rst-noerror-and-end-stream-flow-controlfrom
mosum/http2-3b-preserve-response-on-rst-no-error

Conversation

@mosuem

@mosuem mosuem commented Oct 8, 2026

Copy link
Copy Markdown
Member

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:

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:

00:00 +1: All tests passed!

@mosuem
mosuem added this pull request to stack #2016 October 8, 2026 10:42
@mosuem
mosuem force-pushed the mosum/http2-3b-preserve-response-on-rst-no-error branch 2 times, most recently from 8a90048 to c4f8ca5 Compare October 9, 2026 07:54
@mosuem
mosuem removed this pull request from stack #2016 October 9, 2026 07:55
@mosuem
mosuem added this pull request to stack #2020 October 9, 2026 07:55
@mosuem
mosuem force-pushed the mosum/http2-3b-preserve-response-on-rst-no-error branch from c4f8ca5 to d538899 Compare October 9, 2026 08:18
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

PR Health

Coverage ✔️
File Coverage
pkgs/http2/lib/src/flowcontrol/connection_queues.dart 💚 94 % ⬆️ 1 %
pkgs/http2/lib/src/streams/stream_handler.dart 💚 91 % ⬆️ 0 %

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 skip-coverage-check.

License Headers ✔️
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/http_multi_server/test/cert.dart

This check can be disabled by tagging the PR with skip-license-check.

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?
http2 Non-Breaking 3.1.0 3.2.0-wip 3.2.0-wip ✔️

This check can be disabled by tagging the PR with skip-breaking-check.

Unused Dependencies ✔️
Package Status
http2 ✔️ All dependencies utilized correctly.

For details on how to fix these, see dependency_validator.

This check can be disabled by tagging the PR with skip-unused-dependencies-check.

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.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

> [!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!
```
@mosuem
mosuem force-pushed the mosum/http2-3b-preserve-response-on-rst-no-error branch from d538899 to 12962a5 Compare October 9, 2026 08:48
@mosuem
mosuem marked this pull request as ready for review October 9, 2026 08:54
@mosuem
mosuem requested a review from brianquinlan October 9, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants