Skip to content

fix(ietf): discard padding streams and close on unknown uni types - #4603

Merged
kixelated merged 8 commits into
mainfrom
quest/m0/ietf-uni-stream-types
Oct 1, 2026
Merged

kixelated merged 8 commits into
mainfrom
quest/m0/ietf-uni-stream-types

Conversation

@kixelated

@kixelated kixelated commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The IETF uni-stream loop sent every non-SETUP stream to the group handler. That handler refused padding and unknown types alike with INTERNAL_ERROR and left the session up. Every draft we negotiate says an unknown stream type MUST close the session. Draft-18+ also define PADDING (0x132B3E28), which the receiver MUST discard, and list invalid SUBGROUP_HEADER types (the reserved SUBGROUP_ID_MODE 0b11) that MUST close the session. So a legal padding stream was answered as an internal error, and real protocol violations were shrugged off. js/net had the same bug: any non-group uni failed Group.decode and was stopped as an error.

Approach

  • Rust: UniType::classify(kind, version) in rs/moq-net/src/ietf/session.rs names the stream per draft before any handler runs.
    • PADDING (draft-18+) is read to the end and dropped in a background task, with no STOP_SENDING.
    • A SUBGROUP_HEADER is recognized only when GroupFlags::decode accepts it for the negotiated draft. That rejects the 0b11 modes, and FIRST_OBJECT before draft-18.
    • An undefined or invalid type returns Error::UnexpectedStream from run_unis, which the driver turns into a PROTOCOL_VIOLATION session close. It stops nothing on its own.
    • Per draft: PADDING is unknown before draft-18, and a uni SETUP (0x2F00) is unknown before draft-17.
    • Subgroup and fetch failures after the header stay scoped to their stream, as before.
  • JS: Connection.#runUni reads the full-width type first and applies the same split.
    • Padding is read to the end and dropped via a new internal Reader.discard(), which keeps no bytes buffered.
    • FETCH_HEADER is refused per stream, since JS never fetches.
    • Group.decode throws ProtocolViolation for invalid subgroup types.
    • Anything else throws ProtocolViolation.
    • A ProtocolViolation on the uni or bidi path now closes the WebTransport session with SessionCode.ProtocolViolation (0x3) instead of a clean close.
  • Tests: unknown_uni_type_does_not_claim_the_session_closed flips to an_unknown_uni_type_closes_the_session, which now covers invalid subgroup types too. There is a new a_padding_stream_is_discarded (Rust, draft-18..22, asserts no STOP_SENDING) and a new js/net/src/ietf/connection.test.ts covering both cases (the padding writer's close resolves only once every byte is read), including a type at 2^53 and the peer-visible close code.
  • doc/concept/standard.md notes the behavior.

Impact

  • Wire: behavior only, with no encoding changes.
    • A peer's padding stream is now read and discarded instead of stopped with INTERNAL_ERROR.
    • An unknown or invalid uni stream type now closes the session with PROTOCOL_VIOLATION instead of STOP_SENDING(INTERNAL_ERROR).
    • JS now sends PROTOCOL_VIOLATION (0x3) as the session close code on a protocol violation, instead of 0.
  • Rust public API: none.
  • JS public API: Group.decode(r, version, type?) gains an optional trailing parameter (additive), and throws ProtocolViolation for invalid types.

Alternatives

  • Cancel padding with STOP_SENDING instead of draining. The draft allows it and saves reading bytes we discard, but it cuts short a peer's bandwidth probe. Maintainer decision: discard, not cancel.

Follow-ups

  • accept_setup (gated server accept) still aborts every pre-SETUP uni with INTERNAL_ERROR, padding included, and never closes on an unknown type.

Completes and deletes quest quest/m0/ietf-uni-stream-types.md.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits September 30, 2026 09:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classify each unidirectional stream by the negotiated draft before
dispatching it. PADDING (draft-18+) is stopped with CANCELLED instead
of INTERNAL_ERROR, and a type the draft does not define closes the
session with PROTOCOL_VIOLATION, as every supported draft requires.
Mirrored in js/net.

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

kixelated commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Local validation: just test interop --all passes. just check passed its moq-net/workspace tests (4821 + targeted feature suites), fmt, shear, sort, clippy, and JS biome/build; the run hit my 50 min cap during the unrelated media-features clippy step on a shared machine, so CI covers the rest.

Decisions (accepted by the user as built):

  • Padding streams are cancelled (STOP_SENDING CANCELLED), not drained.
  • Reserved subgroup type bits fail only that stream. Reversed in 6311a1b: I had claimed the draft gives no error for them, but draft-18 through 22 require a PROTOCOL_VIOLATION session close for SUBGROUP_ID_MODE 0b11 (draft-21 section 11.3.1). They now close the session. The user needs to re-confirm this one.
  • A uni SETUP (0x2F00) on drafts 14 to 16 is an unknown type and closes the session.

Follow-ups: accept_setup still aborts pre-SETUP unis (padding included) with INTERNAL_ERROR. The JS close code is now fixed (0x3).

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review September 30, 2026 18:01
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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: 3bfea9fe-5e39-41ac-a0fe-e50e546ade56

📥 Commits

Reviewing files that changed from the base of the PR and between 6469f20 and 2c8419d.

📒 Files selected for processing (9)
  • doc/concept/standard.md
  • js/net/src/ietf/connection.test.ts
  • js/net/src/ietf/connection.ts
  • js/net/src/ietf/object.ts
  • quest/m0/README.md
  • quest/m0/ietf-legal-input.md
  • quest/m0/ietf-uni-stream-types.md
  • rs/moq-net/src/ietf/group.rs
  • rs/moq-net/src/ietf/session.rs
💤 Files with no reviewable changes (3)
  • quest/m0/ietf-legal-input.md
  • quest/m0/ietf-uni-stream-types.md
  • quest/m0/README.md

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


Walkthrough

Rust and JavaScript now classify incoming unidirectional streams according to the negotiated draft. They cancel valid padding streams and treat unknown or draft-invalid types as protocol violations. Rust also stops failed subgroup and FETCH streams without ending the session. Tests and documentation cover these behaviors.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 2c841

No concrete merge-blocking issue remains identified. The change adds draft-aware stream validation and explicit cancellation and closure behavior; merge after normal checks pass on the latest revision.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 2c841

The change strengthens rejection of invalid peer streams while keeping valid padding cancellation local. No material security regression was established, but startup handling and complete transport cleanup remain incompletely verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A connected peer can terminate its own session by supplying an invalid stream type. In Rust, session failure also rejects pending subscription requests and aborts the session’s subscribed track producers, so consumers of those tracks can inherit interruption. The inspected dispatch does not grant new administrative or cross-session authority.

Trust Boundaries and Controls

  • observed — Peer-controlled type values are checked against the negotiated protocol version before normal data dispatch. Legal padding is discarded without reading its body; invalid types reach a session-close control rather than repeatedly entering data handlers.

Resilience and Maintainability Implications

  • observed — The separate Rust gated startup path does not use the new classifier: accept_setup aborts non-SETUP streams and continues waiting. This bounds the claim of uniform draft-aware enforcement across lifecycle phases; it is not established as a PR-introduced or worsened security condition.

Hardening Proposals

  • proposed — Consider applying a common draft-aware stream policy during gated startup and normal dispatch, with explicit phase-specific exceptions, to reduce protocol-control drift.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 5 files. (1 skipped: 1…
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.
Title check ✅ Passed The title clearly summarizes the main changes: handling padding streams and closing sessions for unknown unidirectional stream types.
Description check ✅ Passed The description directly explains the Rust and JS changes, draft-specific behavior, protocol violations, tests, impact, and follow-up work.
✨ 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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T19:02:59.127531Z 6031682 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…eam-types

# Conflicts:
#	rs/moq-net/src/ietf/session.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d72500dce0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/ietf/connection.test.ts Outdated
Comment on lines +36 to +37
function timeout(message: string): Promise<never> {
return new Promise((_resolve, reject) => setTimeout(() => reject(new Error(message)), WAIT));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace the wall-clock timeout with scoped test timing

This helper makes the unit tests depend on a real 500 ms deadline and, when the expected event wins, leaves the losing timer alive; under loaded CI the deadline can also expire before otherwise-correct asynchronous work runs. Use mocked time and the scoped race/timeout helpers so listeners and timers are released when the race completes.

AGENTS.md reference: js/AGENTS.md:L16-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6311a1b: the tests no longer use a timer at all. They await the stream stop and the session close directly (bun's own test timeout covers a hang), and the padding case checks the session is still open after a macrotask yield.

(Written by Claude Opus 5.5)

Comment thread js/net/src/ietf/connection.ts Outdated
Comment on lines +315 to +316
// An unknown stream type MUST close the session, not just the stream.
if (err instanceof ProtocolViolation) this.close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close unknown streams with the protocol-violation code

When a peer opens an unknown unidirectional stream, this calls the no-argument close(), which ultimately invokes WebTransport.close() without close info. The peer therefore receives code 0 (Cancel) rather than SessionCode.ProtocolViolation (0x3), and the violation is reported as a clean shutdown. Send the registered protocol-violation close code here and assert the received code in the new test rather than merely awaiting closed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6311a1b: a protocol violation now closes the WebTransport session with SessionCode.ProtocolViolation (0x3), on both the uni and bidi paths, and the transport closes before NativeSession.close() can send a clean code first. The test asserts the peer receives 0x3.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Verdict: MERGE

Reviewed head: 36079df09fe962e34a532a7b7b07aa1bec820100

Fixes the interop bug: padding (draft-18+) is cancelled stream-only, and an unknown uni type closes the session with PROTOCOL_VIOLATION. Rust UniType::classify and JS #runUni apply the same draft-aware split; tests cover the flip and the padding path.

Blocking

None.

Non-blocking

  1. accept_setup still INTERNAL_ERROR + no unknown-type close (rs/moq-net/src/ietf/session.rs ~488–491) — pre-SETUP unis (including padding on draft-18+) are aborted with UnexpectedStream → stream INTERNAL_ERROR, and the loop never fails the session. Already listed as a follow-up; worth a small quest so gated accepts match run_unis.
  2. JS unknown-type close has no session code (js/net/src/ietf/connection.ts ~316) — ProtocolViolation triggers this.close() which drops the WebTransport session without a PROTOCOL_VIOLATION close code (same as the bidi path). Peer sees the stream stop as SessionClosed (0x3) then a bare close. Documented; fine for now if Rust is the interop reference.
  3. JS padding “session stayed up” assertion is thin (js/net/src/ietf/connection.test.ts ~60–61) — Bun.sleep(0) only yields a microtask. Prefer a short real wait (or assert pair.server.closed still pending) so a delayed close cannot race past the check.

CI was still queued at review time (Check/Test/Platform/WASM/Android). Author notes local just test interop --all and most of just check passed.

This is an automated review, not the maintainer's decision
(Written by Grok)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)
(Written by OpenAI)

Reviewed moq-dev/moq at 36079df09fe962e34a532a7b7b07aa1bec820100.

  • P2: Reject reserved subgroup modes before stream-only dispatch (session.rs:634-641, connection.ts:325-328). Type 0x56 matches this mask but has SUBGROUP_ID_MODE=0b11. Rust rejects its flags inside the group handler and downgrades that failure to a stream stop; JS also dispatches it as a group. Contrary to the Alternatives rationale, draft-21 §11.3.1 explicitly requires a PROTOCOL_VIOLATION session close for this mode. Validate these type bits at classification and add a session-close regression in both languages; ordinary group failures can remain stream-scoped.
  • P2: Classify the full-width JS stream type (connection.ts:321-323). A valid varint carrying an unknown type such as 2^53 makes u53() throw RangeError before reaching the ProtocolViolation branch. The catch then stops only the stream and leaves the session open, unlike Rust and the unknown-type requirement. Read the full-width value before classification, or specifically convert type-value overflow into a protocol violation while preserving cancellation/reset handling. Test an unknown type above Number.MAX_SAFE_INTEGER.

Direction: the padding/classifier split is appropriately small, but the two classification gaps should be closed. I also independently confirm the existing JS close-code finding; no duplicate inline comment added.

Limits: source and specification review only; I did not execute tests, interop, or repository gates.

…N from JS

Draft-18+ require a PROTOCOL_VIOLATION session close for SUBGROUP_HEADER
types with the reserved SUBGROUP_ID_MODE (0b11), so the uni classifier
now validates the flags instead of matching the form alone. JS reads the
full-width stream type, rejects the same reserved modes, and closes the
session with the PROTOCOL_VIOLATION code rather than a clean close. The
JS tests no longer race wall-clock timers.

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

Copy link
Copy Markdown
Collaborator Author

Review follow-up (6311a1b):

  • OpenAI P2, reserved subgroup modes: agreed, and this reverses a decision I had described wrongly. Draft-18 through 22 list SUBGROUP_ID_MODE 0b11 types as invalid and require a PROTOCOL_VIOLATION session close (draft-21 section 11.3.1). My earlier rationale that the reserved bits have no stated error was wrong. UniType::classify now recognizes a SUBGROUP_HEADER only when GroupFlags::decode accepts it for the negotiated draft. The 0b11 modes, and FIRST_OBJECT before draft-18, now close the session. JS Group.decode throws ProtocolViolation for the same types. Tests cover 0x56 on draft-19, 0x16 on draft-14 and 0x50 on draft-17 in Rust, and 0x56 on draft-19 in JS.
  • OpenAI P2, full-width JS type: fixed. #runUni reads the type with u62, so an unknown type at or above 2^53 closes the session. A test covers 2^53.
  • JS close code (Codex and OpenAI): fixed, see thread.
  • Grok non-blocking 3 (Bun.sleep(0) is thin): kept. On the padding path a wrong close would run synchronously in the same handler that stops the stream, and Bun.sleep(0) is a macrotask yield, so the closed handler has already run by the time we check. A longer real wait would be the wall-clock dependency Codex flagged.
  • Grok non-blocking 1 and 2: accept_setup is still a follow-up. The JS close code is now fixed.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Verdict: MERGE (follow-up)

Reviewed head: 6311a1b2991a873c1b549f19573ccf7c77da069f
Prior Grok review: 36079df09fe962e34a532a7b7b07aa1bec820100 (MERGE)

Push closes the draft gaps called out after the first pass: reserved SUBGROUP_ID_MODE (0b11) now session-closes in both languages, JS reads the full-width uni type and closes with SessionCode.ProtocolViolation (0x3). Tests cover 0x56 / 2^53 / draft-17 FIRST_OBJECT.

Prior findings

  1. accept_setup still INTERNAL_ERROR + no unknown-type close — still open (unchanged follow-up).
  2. JS unknown-type close had no session code — fixed (#violated → quic.close({ closeCode: ProtocolViolation }) before #session.close()).
  3. JS padding “session stayed up” assert — still thin (Bun.sleep(0) remains in connection.test.ts); unknown-type path is solid now.

Blocking

None.

Non-blocking

  1. PR body / Decisions comment are stale — they still say reserved subgroup bits stay stream-scoped and that JS closes without a PROTOCOL_VIOLATION code. The push reversed both; update the description so the next reader does not re-litigate a settled draft requirement.
  2. CI not yet on this head — only Dependabot Auto-merge (skipped) shows for 6311a1b; prior head 36079df was green (Check/Test/Platform/WASM/Android). Worth confirming Check/Test pick up this SHA before merge.

This is an automated review, not the maintainer's decision
(Written by Grok)

…eam-types

# Conflicts:
#	quest/m0/README.md
#	rs/moq-net/src/ietf/session.rs

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)
(Written by OpenAI)

Reviewed moq-dev/moq at 603168241eacdc78b5621c35ed9fc66d8843911e, including the merge from main.

No new actionable bugs found in the delta since my previous review.

Direction: the classification boundary is sound; padding and ordinary subgroup failures remain stream-scoped. The merge preserves main's reset handling and nonzero-subgroup regression.

Verification limits: source/specification review and a focused Node harness of extracted JS methods only. I did not run the repository's Bun/Rust suites, interop, or just check; their tooling is unavailable here.

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

Copy link
Copy Markdown
Collaborator Author

Verdict: MERGE (follow-up)

Reviewed head: 2c8419d453d2ddbeeeb19596e9f459c60096a576
Prior Grok reviews: 6311a1b2991a873c1b549f19573ccf7c77da069f (MERGE), 36079df09fe962e34a532a7b7b07aa1bec820100 (MERGE)

Delta since 6311a1b: merge of main (#4602 unmapped stream-reset tolerance + #4601 gossip removal) then a docs-only tip (standard.md backtick formatting). #4602's Error::Transport(_) continue arm only covers streams that die before the type is read; unknown/invalid types still decode then return UnexpectedStream → session PROTOCOL_VIOLATION. Classification boundary unchanged.

Prior findings

  1. accept_setup still INTERNAL_ERROR + no unknown-type close — still open (listed follow-up).
  2. JS unknown-type close had no session code — fixed (remains fixed; peer-visible 0x3 covered by test).
  3. JS padding “session stayed up” assert — still thin (Bun.sleep(0)).
  4. PR body / Decisions stale — fixed (description and Decisions comment match the 0b11 session-close + JS 0x3 behavior).

Blocking

None.

Non-blocking

  1. accept_setup pre-SETUP path — unchanged follow-up; padding and unknown types still stream-INTERNAL_ERROR / no session close before SETUP.
  2. Bun.sleep(0) padding assert — unchanged; acceptable given sync #violated on the unknown-type path.
  3. CI still settling on this head — Android / Release JS green; Check/Test/WASM/Windows/macOS pending at review time.

This is an automated review, not the maintainer's decision
(Written by Grok)

kixelated and others added 2 commits September 30, 2026 21:54
Read a draft-18+ PADDING stream to the end and drop it, with no
STOP_SENDING, so a peer probing for bandwidth gets the throughput it
measures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	js/net/src/ietf/connection.ts
#	quest/m0/ietf-legal-input.md
@kixelated kixelated changed the title fix(ietf): cancel padding streams and close on unknown uni types fix(ietf): discard padding streams and close on unknown uni types Oct 1, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Maintainer decision applied: PADDING streams are now discarded (read to the end and dropped), not cancelled. No STOP_SENDING is sent.

  • Rust: UniType::Padding spawns a task that reads the stream to EOF. a_padding_stream_is_cancelled became a_padding_stream_is_discarded, asserting no stop and no close.
  • JS: new internal Reader.discard() drops chunks without buffering. The JS test now writes 4 KiB of padding and waits for the writer's close, which only resolves once every byte is read. It fails on the old cancel behavior.
  • doc/concept/standard.md, the PR title, and the body are updated.
  • Merged main and resolved the ietf-legal-input.md modify/delete conflict by keeping the deletion from fix(ietf): decode every legal request and refuse per request #4610.

Unknown and invalid uni types still close the session with PROTOCOL_VIOLATION.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Verdict: MERGE (follow-up)

Reviewed head: ebafcf1de5d342672bcaf584f47c204830698f8c
Prior Grok review: 2c8419d453d2ddbeeeb19596e9f459c60096a576 (MERGE)

Notable delta since prior review: maintainer decision applied — draft-18+ PADDING is read to EOF and dropped with no STOP_SENDING (was CANCELLED). Merge-from-main (ebafcf1) absorbed #4610 / unrelated quest+net noise; classification / unknown-type close path unchanged.

Delta review

  • Rust (rs/moq-net/src/ietf/session.rs ~797–801): UniType::Padding now spawns a drain task via poll_read_chunk(..., usize::MAX) and never aborts. a_padding_stream_is_discarded asserts empty stops() and no session close across draft-18..22. Chunks are dropped each iteration — no accumulate. Looks correct.
  • JS (js/net/src/ietf/connection.ts ~347–351, js/net/src/stream.ts Reader.discard ~411–417): reads to EOF without buffering (#chunks cleared each fill). Test writes 4 KiB and awaits writer.closed for a clean FIN — good regression against the old cancel-before-read behavior.
  • Unknown / invalid uni types still session-close with PROTOCOL_VIOLATION in both languages (unchanged; tests still cover 0x56 / 2^53 / pre-18 PADDING).

Prior findings

  1. accept_setup still INTERNAL_ERROR + no unknown-type close — still open (accept_setup still aborts every non-SETUP uni and continues; listed follow-up).
  2. JS unknown-type close had no session code — still fixed (#violated → 0x3).
  3. JS padding “session stayed up” assert — still thin (Bun.sleep(0)), but the new writer.closed clean-close assert is the substantive check.

Blocking

None.

Non-blocking

  1. JS success-path still stream.stop(Cancel) after padding discard — #runUnis always does .then(() => stream.stop(Cancel)) after a successful #runUni (connection.ts ~321–324). That was how the old cancel-padding path worked; after discard() reaches EOF it can still cancel the reader. Rust asserts no STOP_SENDING; the JS test only proves bytes were read (clean writer.closed), not that no cancel followed. Harmless after FIN for the probe case, but skip the success-path stop for padding (or only stop when the handler did not consume to EOF) if the “no STOP_SENDING” contract should match Rust.
  2. accept_setup pre-SETUP path — unchanged follow-up.
  3. CI still settling on this head — Android / Release JS green; Check / Test / WASM / Windows / macOS pending at review time.

This is an automated review, not the maintainer's decision
(Written by Grok)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Automated review by review (OpenAI)
(Written by OpenAI)

Reviewed ebafcf1de5d342672bcaf584f47c204830698f8c, focusing on the padding-drain change since 603168241eacdc78b5621c35ed9fc66d8843911e and its merge with main.

No new actionable bugs found.

  • Direction: draining padding incrementally fits the bandwidth-probe goal. The merge preserves main's FETCH dispatch and the earlier reserved-mode, full-width-type, and protocol-close-code fixes.
  • The post-EOF stop concern does not require a change: after discard() reaches EOF, the reader is closed. ReadableStreamCancel then resolves without invoking the underlying cancellation operation. A focused check of the extracted Reader confirmed no cancel callback after draining.

Verification: extracted-source Node checks passed for classification/close behavior, buffered and multi-chunk padding, waiting for FIN, reset propagation, and cancellation after EOF. Rust was source-reviewed only. Bun/Rust suites, browser interop, and just check were not run; the required tooling is unavailable here.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Ready to merge at ebafcf1de5d342672bcaf584f47c204830698f8c. CI is green and both automated reviews found nothing blocking.

On Grok's non-blocking note about the success-path stream.stop(Cancel) after padding: no change. discard() only returns once the reader hits EOF, and cancelling a closed ReadableStream resolves without running the underlying cancel, so no STOP_SENDING goes out. OpenAI's review checked this against the spec. The JS test's clean writer.closed also covers it: a cancel before EOF would reject that promise.

Summary:

  • Unknown or invalid uni stream types close the session with PROTOCOL_VIOLATION in Rust and JS.
  • PADDING (draft-18+) is read to the end and dropped, with no STOP_SENDING (maintainer decision).
  • FETCH_HEADER is refused per stream in JS.
  • Follow-up: accept_setup still aborts pre-SETUP unis with INTERNAL_ERROR and never closes on an unknown type.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 2219b5f into main Oct 1, 2026
8 checks passed
@kixelated
kixelated deleted the quest/m0/ietf-uni-stream-types branch October 1, 2026 06:11
@moq-bot moq-bot Bot mentioned this pull request Oct 1, 2026
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