Skip to content

Deliver RESET_STREAM on a peer-initiated stream's first frame#12

Merged
rnro merged 2 commits into
apple:mainfrom
rnro:reset_sream_drop
Jun 12, 2026
Merged

Deliver RESET_STREAM on a peer-initiated stream's first frame#12
rnro merged 2 commits into
apple:mainfrom
rnro:reset_sream_drop

Conversation

@rnro

@rnro rnro commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

At the moment a reset arriving before any STREAM frame on a fresh peer stream ID is silently dropped.

RFC 9000 Section 3.2 lists RESET_STREAM among the frames that create the receiving part of a peer-initiated stream, but FrameResetStream bails out before createInboundStreams when knownFlows has no entry -
the inbound-aborted event never reaches the upper protocol and the peer's intent is lost. A second path captures the flow via connection.flow(for:) before createInboundStreams runs and never refreshes the binding, so even when the stream is created in-line the subsequent guard returns without delivering the reset.

We can't naively collapse the two cases (unknown stream ID; present ID with nil flow) into a single createInboundStreams call because RFC 9000 Section 3.5 says STOP_SENDING SHOULD not be sent for a stream the peer has just reset, and createInboundStreams's isDetached early-return enqueues STOP_SENDING to the peer.

This change:

  • Restructures FrameResetStream.process to fetch the stream fresh from knownFlows / multiplexedFlows after createInboundStreams runs, replacing the captured-then-stale flow binding.
  • Splits the lookup so a present-but-flow-nil entry drops directly with a log, and createInboundStreams only runs when the stream ID is genuinely unknown.
  • Documents the !inboundStreamResult.created branch's return true with the reasoning (peer notification or close already handled inside createInboundStreams).
  • Adds runQUICResetStreamFirstFrameOnNewStream and testQUICResetStreamFirstFrameOnNewStream covering the end-to-end RESET-only flow.

After this change a peer-initiated stream that receives RESET_STREAM as its first (and only) frame delivers the inbound-aborted event to the upper protocol. The change avoids spuriously emitting STOP_SENDING in response to a peer reset when the inbound flow handler has detached. The new test passes deterministically; pre-fix it timed out without observing the new flow or the abort.

At the moment a reset arriving before any `STREAM` frame on a fresh
peer stream ID is silently dropped.

RFC 9000 Section 3.2 lists `RESET_STREAM` among the frames that create
the receiving part of a peer-initiated stream, but `FrameResetStream`
bails out before `createInboundStreams` when `knownFlows` has no entry
- the inbound-aborted event never reaches the upper protocol and the
peer's intent is lost. A second path captures the flow via
`connection.flow(for:)` before `createInboundStreams` runs and never
refreshes the binding, so even when the stream is created in-line the
subsequent guard returns without delivering the reset.

We can't naively collapse the two cases (unknown stream ID; present
ID with nil flow) into a single `createInboundStreams` call because
RFC 9000 Section 3.5 says `STOP_SENDING` SHOULD not be sent for a
stream the peer has just reset, and `createInboundStreams`'s
`isDetached` early-return enqueues `STOP_SENDING` to the peer.

This change:

* Restructures `FrameResetStream.process` to fetch the stream fresh
  from `knownFlows` / `multiplexedFlows` after `createInboundStreams`
  runs, replacing the captured-then-stale flow binding.
* Splits the lookup so a present-but-flow-nil entry drops directly
  with a log, and `createInboundStreams` only runs when the stream ID
  is genuinely unknown.
* Documents the `!inboundStreamResult.created` branch's `return true`
  with the reasoning (peer notification or close already handled
  inside `createInboundStreams`).
* Adds `runQUICResetStreamFirstFrameOnNewStream` and
  `testQUICResetStreamFirstFrameOnNewStream` covering the end-to-end
  RESET-only flow.

After this change a peer-initiated stream that receives `RESET_STREAM`
as its first (and only) frame delivers the inbound-aborted event to
the upper protocol. The change avoids spuriously emitting
`STOP_SENDING` in response to a peer reset when the inbound flow
handler has detached. The new test passes deterministically; pre-fix
it timed out without observing the new flow or the abort.

@agnosticdev agnosticdev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding that test case too.

@rnro rnro merged commit cc73bf2 into apple:main Jun 12, 2026
20 checks passed
@rnro rnro deleted the reset_sream_drop branch June 12, 2026 09:56
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.

4 participants