From 7495414f89316172ccf44d49677dd35058dee3cf Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 1 Oct 2026 17:16:53 -0700 Subject: [PATCH 1/2] quest: plan IETF unknown bidi, lite early streams, mux truncated group Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 2 ++ quest/m0/ietf-unknown-bidi.md | 30 ++++++++++++++++++++++ quest/m0/lite-early-streams.md | 44 +++++++++++++++++++++++++++++++++ quest/m1/README.md | 1 + quest/m1/mux-truncated-group.md | 22 +++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 quest/m0/ietf-unknown-bidi.md create mode 100644 quest/m0/lite-early-streams.md create mode 100644 quest/m1/mux-truncated-group.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 2781ce6732..2fcab1aaee 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -42,6 +42,8 @@ Published API or wire breaks still land on dev; each quest's Plan says so. - [IETF FIN semantics](/quest/m0/ietf-fin-not-cancel.md) - a request stream FIN stops updates without cancelling, and REQUEST_UPDATE on a subscribe is parsed - [IETF early streams](/quest/m0/ietf-early-streams.md) - a moq-transport stream that arrives before SETUP is held until SETUP lands, never aborted - [SUBSCRIBE_TRACKS refusal](/quest/m0/ietf-subscribe-tracks.md) - a draft-18+ SUBSCRIBE_TRACKS gets NOT_SUPPORTED on its stream, not a session close +- [JS unknown bidi](/quest/m0/ietf-unknown-bidi.md) - an unknown moq-transport bidi stream type closes the session in JS, as in Rust +- [moq-lite early streams](/quest/m0/lite-early-streams.md) - a lite uni stream that arrives before SETUP is held until SETUP lands, and a second SETUP is fatal - [Request caps](/quest/m0/request-caps.md) - lite message sizes, IETF request IDs, and per-session announces and subscriptions are bounded - [noq reassembly cap](/quest/m0/noq-reassembly-cap.md) - noq carries quinn's stream reassembly cap and the connection receive window is finite by default - [qmux reset race](/quest/m0/qmux-reset-race.md) - qmux handles RESET_STREAM under one lock instead of panicking diff --git a/quest/m0/ietf-unknown-bidi.md b/quest/m0/ietf-unknown-bidi.md new file mode 100644 index 0000000000..1b7e4ab6df --- /dev/null +++ b/quest/m0/ietf-unknown-bidi.md @@ -0,0 +1,30 @@ +# [XS] JS closes an IETF session on an unknown bidi stream type + +## Goal + +In `@moq/net`, a moq-transport bidi stream whose message type is unknown closes +the session with PROTOCOL_VIOLATION, as Rust and the JS uni path already do, +instead of aborting only that stream. A message a draft defines but we don't +support keeps the refuse-per-request rule +(#4685, #4610). + +## Plan + +Facts (`origin/main`, 2026-10-01): + +- JS: the `default:` arm of the bidi dispatch in + `js/net/src/ietf/connection.ts` logs and calls `stream.abort(...)`, so the + `#runBidis` catch never reaches `#violated` and the session stays open. +- JS uni already throws `ProtocolViolation` for an unknown type. +- Rust: `run_dispatch`'s `_` arm in `rs/moq-net/src/ietf/session.rs` returns + `UnexpectedStream`, which maps to PROTOCOL_VIOLATION. Rust has a uni test + (`an_unknown_uni_type_closes_the_session`) but no bidi one. + +Decided (2026-10-01): unknown is fatal; defined but unsupported is refused per +request. Rejected: making Rust lenient to match JS. + +Work: throw `ProtocolViolation` from the JS default arm, and add a bidi test in +both languages. moq-lite is out of scope: its draft says an unknown stream type +MUST be reset and MUST NOT be fatal, and both implementations comply. + +Public API: none. Wire: none; fixes conformance. diff --git a/quest/m0/lite-early-streams.md b/quest/m0/lite-early-streams.md new file mode 100644 index 0000000000..a477de3fa2 --- /dev/null +++ b/quest/m0/lite-early-streams.md @@ -0,0 +1,44 @@ +# [S] moq-lite streams that arrive before SETUP are held + +## Goal + +On lite-05 and later (the versions with a Setup Stream), a uni stream that +arrives before the peer's SETUP is held and handled once SETUP lands, never +aborted, in Rust and JS. A second Setup Stream closes the session with +PROTOCOL_VIOLATION in both languages, as the lite draft requires. + +## Plan + +Facts (`origin/main`, 2026-10-01): + +- Rust `lite::accept_setup` (`rs/moq-net/src/lite/session.rs`) aborts every + non-SETUP uni stream with `UnexpectedStream`. + `accept_request_skips_uni_stream_before_setup` in `rs/moq-net/src/server.rs` + pins that. An unknown `DataType` fails the handshake. +- JS lite has no pre-SETUP gate: `#runUni` in `js/net/src/lite/connection.ts` + handles SETUP like any other uni stream, so early groups already work. It + also accepts a second SETUP silently. +- `drafts/draft-lcurley-moq-lite.md`: neither endpoint waits for the peer's + SETUP. A receiver MUST buffer only streams whose encoding depends on a + negotiated extension, and a second Setup Stream MUST close with + PROTOCOL_VIOLATION. Rust's abort is stricter than the draft. +- Lite-01 to lite-04 have no Setup Stream and are out of scope. + +Decided (2026-10-01): + +- ✅ Rust holds early uni streams until SETUP, then hands them to the normal + classifier, the same shape as + IETF early streams (#4686). Rust can't + route before SETUP anyway, since path and role come from it. Rejected: + processing immediately unless extension-dependent, which needs a session + before SETUP. +- ✅ The duplicate-SETUP check rides along, in both languages. + +Crib from #4686: its `died_before_header` helper and peek-then-hold pattern. +QUIC stream credit bounds the held queue. Check whether JS lite must buffer any +extension-dependent stream type today, and hold those until SETUP. + +Tests: flip `accept_request_skips_uni_stream_before_setup` to expect the group +to be delivered. In both languages, a duplicate SETUP closes the session. + +Public API: none. Wire: none; matches the lite draft. diff --git a/quest/m1/README.md b/quest/m1/README.md index 80f417690a..a035aa33ff 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -28,6 +28,7 @@ QUIC studies there on that rule. - [One route cost](/quest/m1/route-cost.md) - on `dev`, Warm and Cold collapse to one static route cost - [Delete removed cluster flags](/quest/m1/cluster-shims.md) - on `dev`, once a release has carried their refusals, `mesh` and `linger` leave `cluster::Config` - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` +- [moq-mux truncated group](/quest/m1/mux-truncated-group.md) - a test pins that moq-mux skips a truncated spliced group as aborted - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below diff --git a/quest/m1/mux-truncated-group.md b/quest/m1/mux-truncated-group.md new file mode 100644 index 0000000000..888b1ea6bd --- /dev/null +++ b/quest/m1/mux-truncated-group.md @@ -0,0 +1,22 @@ +# [XS] moq-mux skips a truncated spliced group + +## Goal + +A moq-mux test feeds its consumer a spliced group whose continuation can never +arrive and asserts the group is skipped as aborted, not read as a short clean +group. + +## Plan + +#4689 made `group::Consumer` reads and `finished()` fail on a truncated +spliced group (`Error::Dropped` for a pruned seam, else the dead route's +error), where they used to end clean. moq-mux's consumer already drops a group +whose read fails, but only code reading verified that; no test covers a spliced +group. Build one with the moq-net resume test fixtures, or the smallest public +path to a pruned seam. + +Public API: none. Wire: none. + +## Related + +- [Resumed groups](/quest/m1/resume-latest.md) - same `resume.rs` splice path From e81cfdf130a4cc22077c39f3b080a7b9af4c0a73 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 1 Oct 2026 17:29:45 -0700 Subject: [PATCH 2/2] quest: address review on unknown bidi, lite early streams, mux truncated group Co-Authored-By: Claude Opus 5.5 --- quest/m0/ietf-unknown-bidi.md | 10 ++++++++-- quest/m0/lite-early-streams.md | 12 ++++++++++-- quest/m1/mux-truncated-group.md | 9 +++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/quest/m0/ietf-unknown-bidi.md b/quest/m0/ietf-unknown-bidi.md index 1b7e4ab6df..e075b86053 100644 --- a/quest/m0/ietf-unknown-bidi.md +++ b/quest/m0/ietf-unknown-bidi.md @@ -23,8 +23,14 @@ Facts (`origin/main`, 2026-10-01): Decided (2026-10-01): unknown is fatal; defined but unsupported is refused per request. Rejected: making Rust lenient to match JS. -Work: throw `ProtocolViolation` from the JS default arm, and add a bidi test in -both languages. moq-lite is out of scope: its draft says an unknown stream type +- JS reads the bidi type with `u53()`, which throws `RangeError` for a valid + varint above 2^53 before the switch runs, so `#runBidis` never sees a + `ProtocolViolation` for it either. The uni path already reads `u62()`. + +Work: read the bidi type at full wire width (`u62()`), throw +`ProtocolViolation` from the JS default arm, and add a bidi test in both +languages that asserts the PROTOCOL_VIOLATION close code for a small unknown +type and one at or above 2^53. moq-lite is out of scope: its draft says an unknown stream type MUST be reset and MUST NOT be fatal, and both implementations comply. Public API: none. Wire: none; fixes conformance. diff --git a/quest/m0/lite-early-streams.md b/quest/m0/lite-early-streams.md index a477de3fa2..b9d005c299 100644 --- a/quest/m0/lite-early-streams.md +++ b/quest/m0/lite-early-streams.md @@ -17,7 +17,10 @@ Facts (`origin/main`, 2026-10-01): pins that. An unknown `DataType` fails the handshake. - JS lite has no pre-SETUP gate: `#runUni` in `js/net/src/lite/connection.ts` handles SETUP like any other uni stream, so early groups already work. It - also accepts a second SETUP silently. + also accepts a second SETUP silently, and the `#runUnis` catch only stops the + stream, so throwing `ProtocolViolation` there would not close the session. + The `#runBidis` catch closes on `ProtocolViolation`, but with a bare + `close()` rather than the PROTOCOL_VIOLATION code. - `drafts/draft-lcurley-moq-lite.md`: neither endpoint waits for the peer's SETUP. A receiver MUST buffer only streams whose encoding depends on a negotiated extension, and a second Setup Stream MUST close with @@ -32,7 +35,12 @@ Decided (2026-10-01): route before SETUP anyway, since path and role come from it. Rejected: processing immediately unless extension-dependent, which needs a session before SETUP. -- ✅ The duplicate-SETUP check rides along, in both languages. +- ✅ The duplicate-SETUP check rides along, in both languages. In JS the uni + catch must close the session with the PROTOCOL_VIOLATION code, not copy the + bidi path's bare `close()`. + +The remaining JS gap is the duplicate SETUP and its close path; the hold is +Rust work unless JS turns out to need an extension-dependent hold. Crib from #4686: its `died_before_header` helper and peek-then-hold pattern. QUIC stream credit bounds the held queue. Check whether JS lite must buffer any diff --git a/quest/m1/mux-truncated-group.md b/quest/m1/mux-truncated-group.md index 888b1ea6bd..fb44cad34f 100644 --- a/quest/m1/mux-truncated-group.md +++ b/quest/m1/mux-truncated-group.md @@ -8,9 +8,9 @@ group. ## Plan -#4689 made `group::Consumer` reads and `finished()` fail on a truncated -spliced group (`Error::Dropped` for a pruned seam, else the dead route's -error), where they used to end clean. moq-mux's consumer already drops a group +Start after #4689 lands: it makes `group::Consumer` reads and `finished()` fail +on a truncated spliced group (`Error::Dropped` for a pruned seam, else the dead +route's error), where they used to end clean. moq-mux's consumer already drops a group whose read fails, but only code reading verified that; no test covers a spliced group. Build one with the moq-net resume test fixtures, or the smallest public path to a pruned seam. @@ -19,4 +19,5 @@ Public API: none. Wire: none. ## Related -- [Resumed groups](/quest/m1/resume-latest.md) - same `resume.rs` splice path +- [#4689](https://github.com/moq-dev/moq/pull/4689) - the truncated-group behavior this test pins +- [#4491](https://github.com/moq-dev/moq/pull/4491) - the `resume.rs` splice path