Deliver RESET_STREAM on a peer-initiated stream's first frame#12
Merged
Conversation
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
approved these changes
Jun 11, 2026
agnosticdev
left a comment
Collaborator
There was a problem hiding this comment.
Thank you for adding that test case too.
tfpauly
approved these changes
Jun 11, 2026
ekinnear
approved these changes
Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
At the moment a reset arriving before any
STREAMframe on a fresh peer stream ID is silently dropped.RFC 9000 Section 3.2 lists
RESET_STREAMamong the frames that create the receiving part of a peer-initiated stream, butFrameResetStreambails out beforecreateInboundStreamswhenknownFlowshas 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:)beforecreateInboundStreamsruns 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
createInboundStreamscall because RFC 9000 Section 3.5 saysSTOP_SENDINGSHOULD not be sent for a stream the peer has just reset, andcreateInboundStreams'sisDetachedearly-return enqueuesSTOP_SENDINGto the peer.This change:
FrameResetStream.processto fetch the stream fresh fromknownFlows/multiplexedFlowsaftercreateInboundStreamsruns, replacing the captured-then-stale flow binding.createInboundStreamsonly runs when the stream ID is genuinely unknown.!inboundStreamResult.createdbranch'sreturn truewith the reasoning (peer notification or close already handled insidecreateInboundStreams).runQUICResetStreamFirstFrameOnNewStreamandtestQUICResetStreamFirstFrameOnNewStreamcovering the end-to-end RESET-only flow.After this change a peer-initiated stream that receives
RESET_STREAMas its first (and only) frame delivers the inbound-aborted event to the upper protocol. The change avoids spuriously emittingSTOP_SENDINGin 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.