Skip to content

Discard late HEADERS for locally reset streams - #33

Merged
ioquatix merged 2 commits into
socketry:mainfrom
ekmartin:ek-conductor/late-headers-after-reset
Sep 27, 2026
Merged

ioquatix merged 2 commits into
socketry:mainfrom
ekmartin:ek-conductor/late-headers-after-reset

Conversation

@ekmartin

@ekmartin ekmartin commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Decode and discard HEADERS received for a locally-initiated stream that has already been closed, instead of treating them as a new incoming stream.
  • Set the stream ID on CONTINUATION frames generated by Continued#pack, which previously defaulted to 0.

Rationale

When a client resets a stream, e.g. with RST_STREAM(CANCEL) after a request is interrupted, the server may already have the response headers in flight. Connection#receive_headers did not find the stream in @streams, fell through to accept_stream, and failed the whole connection with ProtocolError: Invalid stream id, taking every other in-flight request on the connection down with it. This surfaced with the cancellation cleanup in socketry/async-http#251.

RFC 9113 §5.1 requires an endpoint to process the connection-level effects of frames that arrive after it sent RST_STREAM. For HEADERS, that means the header block must still be HPACK-decoded, otherwise the decoder's dynamic table diverges from the peer's encoder and later responses are decoded incorrectly. The decoded headers are then discarded, and the stream is not recreated. This follows the existing closed_stream_id? pattern in receive_data.

The following are unchanged and still raise:

  • HEADERS on stream 0.
  • HEADERS for idle or invalid stream IDs.
  • Remote-initiated stream ID reuse.
  • Malformed header blocks (COMPRESSION_ERROR).
  • Invalid CONTINUATION sequences.

While adding coverage for split header blocks, I found that Continued#pack created CONTINUATION frames with stream ID 0, so a peer would reject any header block larger than the maximum frame size.

🤖 Generated with Claude Code

When a stream is reset locally, e.g. by cancelling a request, the remote peer may already have response headers in flight. These were treated as a new incoming stream and failed the connection with "Invalid stream id". They are now decoded, to keep the HPACK decoder state synchronized (RFC 9113 §5.1), and discarded.

Also set the stream ID of generated `CONTINUATION` frames, which previously defaulted to 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ekmartin

Copy link
Copy Markdown
Contributor Author

@ioquatix Would you mind taking a look at this? Feel free to fix in your own way if you prefer, I just thought I'd put up a proposal!

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ioquatix

Copy link
Copy Markdown
Member

Thanks, this looks reasonable to me.

@ioquatix
ioquatix merged commit 9e2dfc0 into socketry:main Sep 27, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants