Skip to content

fix(web-transport-moq): send raw QUIC stream codes as is - #27

Merged
kixelated merged 1 commit into
mainfrom
fix/raw-stream-codes
Oct 1, 2026
Merged

kixelated merged 1 commit into
mainfrom
fix/raw-stream-codes

Conversation

@kixelated

Copy link
Copy Markdown

A raw QUIC session (Session::raw) mapped RESET_STREAM and STOP_SENDING codes through the HTTP/3 WebTransport code space, the same mix-up #11 fixed for close codes. Against moxygen over raw QUIC, moq's CANCELLED (1) arrived as 0x52e4a40fa8dc, and moxygen's CANCELLED reached moq as invalid RESET_STREAM: 1 (moq-dev/moq#4602).

  • CloseReason, which already knows whether the session is raw, encodes and decodes stream codes for the session's streams.
  • Raw sessions send reset and stop codes as is, and read a peer's code as is when it fits in a u32.
  • A code in the HTTP/3 WebTransport range still decodes on a raw session. Older web-transport-moq raw peers send that form, and no u32 reaches that range, so it cannot be mistaken for a raw code.
  • HTTP/3 sessions are unchanged.

Impact

  • Public API: none. The public From<noq::ReadError> / From<noq::WriteError> impls keep their HTTP/3 decoding; the streams decode through the session instead.
  • Wire (raw QUIC only): stream codes become the application's own values. A fixed peer reads an older peer's mapped codes. An older peer reads a fixed peer's plain codes as InvalidReset / InvalidStopped, which moq-net reports as a transport error rather than a stream code. See moq-dev/moq's quest/m1/raw-stream-codes.md for what that costs across moq versions.

Tests

tests/raw_stream_codes.rs drives a raw session against a plain noq::Connection: reset and stop in each direction arrive verbatim, and a legacy HTTP/3-mapped code from the plain peer still decodes. The four verbatim cases fail without the fix; the legacy case guards compatibility and passes both ways. Unit tests pin the HTTP/3 mapping for non-raw sessions.

Upstream: none, web-transport-moq is fork-only.

Follow-up: a 1.3.x backport for moq's main (web-transport-trait 0.4) is a separate decision; see the moq PR.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

A raw QUIC session mapped RESET_STREAM and STOP_SENDING codes through the
HTTP/3 WebTransport code space, so a plain QUIC peer saw CANCELLED (1) as
0x52e4a40fa8dc and its own codes read back as invalid. Raw sessions now send
the code as is and read a plain code as is. A code in the HTTP/3 range still
decodes, since older raw peers send that form and no u32 reaches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Author

The maintainer accepted the raw QUIC wire change for moq's dev branch, with no 1.3.x backport. Decision and release order: moq-dev/moq#4614 (comment)

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 1, 2026 23:43

@kixelated kixelated left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No blocking findings.

Low (test coverage): tests/raw_stream_codes.rs only exercises uni streams. Bidi streams go through the same SendStream::new / RecvStream::new with self.close (session.rs:251-252, 291-292), so this is not a bug today, just an untested path.

What I checked:

  • Every path uses the session's raw flag. reset (send.rs:45) and stop (recv.rs:45) encode via CloseReason::encode_stream_code. read, read_exact, read_chunk, read_many_chunks, read_to_end, write, write_all, write_chunk, write_many_chunks, write_all_chunks all decode through the new map_error. received_reset and stopped use decode_stream_code, and the web_transport_trait closed() impls go through those. No other .reset( / .stop( call with an app code exists in the crate.
  • Open and accept. All stream construction (session.rs:238, 251-252, 272, 291-292, 815, 896-897) passes the session's close, which Session::raw builds with CloseReason::new(true) (session.rs:547) and the HTTP/3 path with new(false) (session.rs:121).
  • HTTP/3 unchanged. Non-raw encode is the old error_to_http3 + try_from. Non-raw decode is error_from_http3 only, so a small code is still InvalidReset / InvalidStopped / None, as before. The unit test pins this.
  • u32 / VarInt. Raw encode is the infallible u32 -> VarInt. Raw decode tries the HTTP/3 range first, then u32::try_from. error_to_http3(0) is already past u32::MAX, so the two forms can't overlap. Codes past u32 that aren't in the HTTP/3 range become InvalidReset / InvalidStopped / None.
  • Poll APIs. AsyncRead::poll_read and AsyncWrite::poll_* forward noq's io::Error without mapping the code. That was already true before this PR and stays the same for both session kinds.
  • Header-read path. read_varint / log_header_error (session.rs:930-955) only match Reset(_) and don't decode the code, so nothing changes there.
  • Parity with moq-dev/web-transport#408. Same encode_stream_code / decode_stream_code logic and the same map_error split. The one difference is that the raw flag lives on the shared CloseReason instead of a per-stream raw: bool, which fits this crate because it already tracks raw-ness there for close codes (#11).
  • Tests fail without the fix. I swapped web-transport-moq/src back to the base (0aa0af85) and ran tests/raw_stream_codes.rs. reset_is_sent_as_is, stop_is_sent_as_is, peer_reset_is_read_as_is and peer_stop_is_read_as_is fail, and legacy_peer_codes_are_unmapped passes, which matches the description. On the PR head the whole web-transport-moq suite passes.

Impact (agrees with the description):

  • Public API: none. The From<noq::ReadError> / From<noq::WriteError> impls still decode HTTP/3 only, so external callers using them on a raw connection keep the old behavior.
  • Wire (raw QUIC only), compatible in one direction only:
    • A fixed peer reads an older raw peer's mapped codes correctly.
    • An older raw peer sees a fixed peer's plain codes as InvalidReset / InvalidStopped, and None from received_reset / stopped.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b261551a-04e8-40f6-a121-1dbfc75b7447

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa0af8 and 7a86a21.

📒 Files selected for processing (4)
  • web-transport-moq/src/error.rs
  • web-transport-moq/src/recv.rs
  • web-transport-moq/src/send.rs
  • web-transport-moq/tests/raw_stream_codes.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

CloseReason now encodes and decodes stream error codes based on whether a session uses raw QUIC. Receive and send stream operations use these conversions for reset and stop codes. Tests cover WebTransport mappings, raw code exchange with a plain QUIC peer, and legacy HTTP/3-mapped codes.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 7a86a

Raw QUIC sessions now send reset and stop codes unchanged, and WebTransport behavior is preserved. I found no merge-blocking risk in the supplied change.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 7a86a

Updated raw QUIC peers accept legacy codes, but older peers reject the newly emitted plain codes. This can change cancellation error handling during mixed-version operation. The inspected paths preserve session ownership and do not introduce new authentication or privilege authority.

Retained concerns

  • Low · architecture · observed: Wire compatibility is one-way during mixed-version operation or rollback: updated readers accept legacy mapped codes, but legacy readers reject updated senders' plain reset and stop codes. The PR reports that moq-net classifies these as transport errors rather than application-stream errors. Downstream failure-containment consequences were not supplied.
Security review details

Security Blast Radius

  • inferred — A connected peer controls its stream reset and stop codes, but the inspected adapter paths expose those values as results for the affected stream. They do not grant authority over other streams or mutate the shared session close reason. Wider downstream effects of legacy transport-error classification remain unverified.

Trust Boundaries and Controls

  • observed — Session construction selects the immutable raw-mode flag before stream decoding. Peer-provided codes cannot select that mode. Non-raw decoding rejects unmapped codes, and raw fallback uses checked u32 conversion rather than truncation. Session close-state recording remains separate from stream-code decoding.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: raw QUIC stream codes are sent unchanged.
Description check ✅ Passed The description explains the bug, implementation, wire behavior, compatibility impact, tests, and follow-up decisions. It does not reproduce the template headings or checklist, but it provides the req…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kixelated

Copy link
Copy Markdown
Author

Thanks. On the bidi coverage note: bidi streams are built through the same SendStream::new / RecvStream::new with the session's close, so the uni tests cover the code path. Leaving it as is rather than duplicating the four cases.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit decf515 into main Oct 1, 2026
2 checks passed
kixelated added a commit that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated mentioned this pull request Oct 2, 2026
kixelated added a commit that referenced this pull request Oct 2, 2026
* fix(proto): yield error on too many gaps in assembler

Port of quinn-rs/quinn@fed0321a (quinn-rs/quinn#2694, RUSTSEC-2026-0185):
a peer sending small gapped STREAM or CRYPTO frames could make the
assembler buffer an unbounded number of chunks. After defragmenting,
more than 1024 chunks now closes the connection with INTERNAL_ERROR.

Co-Authored-By: Luke Curley <kixelated@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(proto): bound assembler chunk count regardless of over-allocation

Port of quinn-rs/quinn@4b88c940 (quinn-rs/quinn#2789). The cap above
was only checked inside the over-allocation branch, so gapped frames
whose allocation matches their length, or frames handled entirely by
the unordered dedup loop, still grew the chunk count without bound.
Compact once the count passes twice the cap and reject only if
compaction cannot bring it back under.

Co-Authored-By: Luke Curley <kixelated@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(proto): coalesce contiguous chunks during defragment

Port of quinn-rs/quinn@c3f8b098 (quinn-rs/quinn#2814). defragment now
combines contiguous chunks smaller than buffered / MAX_CHUNKS, so a
large stream_receive_window with a slow in-order reader no longer trips
TooManyChunks on lossless data. Chunks above the minimum size are left
alone so they are not copied on every compaction.

Co-Authored-By: Luke Curley <kixelated@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: release 2.0.1

Bumps the shared workspace version and the internal pins, and records
#25 (reassembly cap, RUSTSEC-2026-0185), #19, and #23 in
CHANGELOG-MOQ.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: cover #27 and the unordered compaction fix in 2.0.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dirkjan Ochtman <dirkjan@ochtman.nl>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Otto Hermann <code@otto-hermann.me>
Co-authored-by: Eanna Lyons <eanna@magic.dev>
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.

1 participant