diff --git a/quest/m1/README.md b/quest/m1/README.md index 17b74156dc..b4143a351d 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -43,6 +43,10 @@ bench all require its crate. Added 2026-10-10 by maintainer priority: HTTP fetch stats, ranked just above session outcomes, and embedder refusals inside session outcomes. +Imported 2026-10-12 from GitHub issues: the 0.17 warm FETCH stall and the +moq-lite malformed-input closes rank just below Dropped sources, and #5347 +folded into Dropped sources. + ## Required - [Interop close logs](/quest/m1/interop-close-log.md) - logging fix landed; delete once the required Interop run is green @@ -95,7 +99,9 @@ session outcomes, and embedder refusals inside session outcomes. - [IETF takeover](/quest/m1/ietf-takeover.md) - carry the shared takeover contract through the negotiated IETF epoch extension - [Identity and recovery guide](/quest/m1/identity-guide.md) - explain pinned catalogs, lazy jobs, takeover, restart and version compatibility - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it -- [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` +- [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped`; a dead route ends Unroutable, and the TS follower lingers on announcements instead of matching errors +- [Release warm FETCH](/quest/m1/release-warm-fetch.md) - on the 0.17 line, a FETCH of a group a subscription delivered in part gets every frame instead of stalling on a parked open group +- [Malformed moq-lite input](/quest/m1/lite-malformed-close.md) - a lite peer that breaks a control-stream rule, including SETUP's client-only parameters, gets PROTOCOL_VIOLATION, and a Report-level publisher answers PROBE targets, in Rust and js/net - [Client settings parity](/quest/m1/obs-client-config.md) - moq-ffi offers moq-c's client knobs, and the OBS Advanced settings get back the ones the C++ migration dropped - [Session report parity](/quest/m1/obs-session-report.md) - a session reports its negotiated draft and reconnect failures, so the OBS dock shows them again - [Generated C++ shape](/quest/m1/cpp-generated-shape.md) - `moq::Client` is the generated type itself and `moq::expected` is one type at every C++ standard diff --git a/quest/m1/dropped-sources.md b/quest/m1/dropped-sources.md index c185cc6188..a63efbfd81 100644 --- a/quest/m1/dropped-sources.md +++ b/quest/m1/dropped-sources.md @@ -1,4 +1,4 @@ -# [M] Consumers see the producer's real error, never Dropped +# [L] Consumers see the producer's real error, never Dropped ## Goal @@ -6,6 +6,7 @@ A track that ends because its source ended reports the source's own error to every consumer, locally and across a relay. `Dropped` means only that a handle was dropped without an end, which a correct producer never does. A broadcast end carries no cause since #4031, so only track errors are in scope. +Consumers never decide behavior by matching an error; errors are diagnostics. ## Plan @@ -44,12 +45,48 @@ end carries no cause since #4031, so only track errors are in scope. exact error, never `internal error`; a subscribe against a withdrawn route expects `Unroutable`. +- Decided 2026-10-12 from [#5347](https://github.com/moq-dev/moq/issues/5347): + `moq export ts --linger --stitch` exited on `internal error` when its + catalog TRACK request landed on a publisher session that was dead but not + yet timed out. On epochless lite-06 the front is offered only its first + route, so when that session timed out, `Front::selected(None)` ended the + resolved front with `Dropped`, `TrackIo::end` rejected the request with it, + and it went out as Internal. A `Dropped` that reaches the wire means some + path never ended with a proper error; diagnose each one at its root rather + than remapping it. + - A route that fails under a request or track (the upstream session dies or + times out, and nothing replaces the copy) ends downstream with + `Unroutable`, for pending requests and for tracks whose copy ends with the + upstream's `Transport` or `Session` error. The downstream session is + healthy; only the path lost its route. This matches + `resume::route_failed`, which already counts those as a route failure. + - Give every variant the catch-alls swallow a real arm, `Transport` + included (also `TooManyRequests`, `Unsupported`, `Duplicate`, + `InvalidRange`, `FrameOpen`, `Encode`, `InvalidJoiningRequestId`). + - Remote errors stay nested (`Error::Stream(..)`), as `Error::stream()` + promises; nothing depends on flat values once the follower stops + matching. Flattening was considered and dropped on #5358's review. + - `ts::Follower` stops matching errors. While lingering, announcements + decide whether the broadcast returned: any resolve error returns it to + waiting for the next announcement, and a new announcement or end cancels + an in-flight resolve instead of sitting out a dead session's idle + timeout. `--linger` is the only exit; at expiry it fails with the last + resolve error if there was one, otherwise with the original end. + - Regressions: a relay test where a TRACK request is in flight to an + upstream session that then times out, and the downstream reset is + Unroutable; a follower test where a resolve error while lingering keeps + lingering until the instance returns, and a refusal surfaces at expiry. + Public API: none expected; error values consumers observe change. Wire: none. ## Required - [Auth](/quest/m1/auth/README.md) - its Unauthorized quest (#4179, done on the line) supplies shared Unauthorized errors and revoked-stream handling +## Closes + +- [#5347](https://github.com/moq-dev/moq/issues/5347) - `export ts --stitch --linger` exits on `internal error` when a catalog request lands on a dying publisher + ## Related - [#4179](https://github.com/moq-dev/moq/pull/4179) - owns the revoked-upstream path diff --git a/quest/m1/lite-malformed-close.md b/quest/m1/lite-malformed-close.md new file mode 100644 index 0000000000..baadda1281 --- /dev/null +++ b/quest/m1/lite-malformed-close.md @@ -0,0 +1,68 @@ +# [M] Malformed moq-lite input closes the session + +## Goal + +On moq-lite, a peer that breaks a control-stream rule gets its session +closed with PROTOCOL_VIOLATION, in both `rs/moq-net` and `js/net`, and a +Report-level publisher answers PROBE targets instead of resetting the stream. +The rules are unchanged from lite-06 through lite-07-wip, so the fixes apply +to every lite version. Data-stream (FRAME) decode errors stay per-stream, as +the draft says, and a reset or FIN is never a violation. + +## Plan + +Found by an external interop runner and verified against the code 2026-10-12. +Rust-to-Rust and Rust-to-JS sessions never hit these; third-party peers do. + +- SETUP (#5257). A client that receives Path or Role in the server's SETUP + closes with PROTOCOL_VIOLATION. `lite/setup.rs` decodes Role through + `Role::from_code`, which turns 0 and unknown values into `None`, so keep + the parameter's presence; a server still reads an unknown Role as Both. + Check at the stored peer SETUP (`lite/subscriber.rs` `UniState::Setup`, + `js/net/src/lite/connection.ts`), which already closes on a + ProtocolViolation. Decided 2026-10-12: also make the server close on Path + over a binding whose handshake carries a request URI (WebTransport, + WebSocket), since the draft sentence is the same. +- PROBE (#5258). `ProbeServe::poll_probe` in `lite/publisher.rs` polls + `reader.poll_closed`, which fails on any pending byte, so the first target + resets the stream with MALFORMED_TRACK. Decode targets with `lite::Probe` + instead; at Report level ignore the value and keep reporting, and only FIN + or a reset ends the stream. In JS, `runProbe` never reads the stream, so a + buffered target probably hides the subscriber's FIN; read targets there + too. Decided 2026-10-12: our subscribers keep sending no target. Unit-test + the publishers; the Increase-level response belongs to + [Discover capacity](/quest/m1/quic-probe.md). +- Control decode errors (#5259). Decided 2026-10-12: any decode error on a + control stream closes the session with PROTOCOL_VIOLATION, not just the two + cases cited (GOAWAY URI over 8,192 bytes, Message Length mismatch). + `Control::poll` closes on `Error::Decode(_)` instead of logging. Delete the + comment that a malformed GOAWAY must not tear down a draining session: the + draft says MUST. Do the same in JS `#runBidis`, which closes today only on + `ProtocolViolation`. Also enforce that a server receiving a non-empty + GOAWAY URI closes the session. Land the PROBE fix first or in the same PR, + or a third-party PROBE target would close the whole session. +- The rule reaches every control decoder, not only `Control::poll` and + `#runBidis`. Child handlers that swallow a peer's decode error must + propagate it: `RequestServe::poll` resets and returns `Ok(())` for a + malformed SUBSCRIBE_UPDATE decoded after `RequestState::Decode`, and + `ProbeServe::poll` consumes `poll_probe`'s error. Response decoders on + streams we opened count too: Rust `RecvBandwidth::poll` logs a malformed + PROBE report from `ProbeStream::poll`, and JS `Subscriber.runProbe` catches + it. Prefer one shared close path over per-handler fixes. +- One test per rule per language, driving raw bytes like the runner does, + including a malformed SUBSCRIBE_UPDATE and a malformed PROBE report. + Share the session-close helper from + [AUTH protocol violations](/quest/m1/auth/violations.md) if it lands first. + +Public API: none. Wire: none; the behavior now matches the draft. + +## Closes + +- [#5257](https://github.com/moq-dev/moq/issues/5257) - client accepts Path or Role in the server's SETUP +- [#5258](https://github.com/moq-dev/moq/issues/5258) - Report-level publisher resets the Probe Stream on a target +- [#5259](https://github.com/moq-dev/moq/issues/5259) - control decode errors end only the stream + +## Related + +- [Malformed moq-transport input](/quest/m2/ietf-malformed-close.md) - the same rule for draft-18 and draft-21 +- [AUTH protocol violations](/quest/m1/auth/violations.md) - the AUTH-only slice, in progress diff --git a/quest/m1/release-warm-fetch.md b/quest/m1/release-warm-fetch.md new file mode 100644 index 0000000000..e5da0415e8 --- /dev/null +++ b/quest/m1/release-warm-fetch.md @@ -0,0 +1,47 @@ +# [S] A FETCH on release never stalls on a parked open group + +## Goal + +On the 0.17 `release` line, a FETCH of a group that a live subscription +delivered only in part, after that subscription ended, receives every frame +the publisher writes and ends with the group. Release only: `main` deleted the +warm copy in #4741 and is unaffected. Lands with the next scheduled 0.17.x; no +dedicated cut. + +## Plan + +Reported in #5348 and bisected to b425d430f (the #4922 backport). Traced +2026-10-12; the pin check the commit message mentions is not the cause. + +- Mechanism: the backport puts every viewer session on one front, so viewer + B's FETCH joins viewer A's parked front. When A's subscription ends + mid-group, `Action::Park` runs `warm_copy`, and `warm_rebuild` copies the + open live-edge group into a new group that it only finishes if the live one + finished. The upstream subscription is already cancelled, so nothing ever + writes to that group, and it is not aborted until `TRACK_IDLE_LINGER`. + `resume::Consumer::cached_group` then reaches `track::covering_group`, which + accepts any group that isn't aborted, finished or not. The FETCH gets the + cached frames and then waits forever. +- Decided 2026-10-12: an unfinished group in a warm segment is a cache miss + for FETCH. `cached_group` (`model/resume.rs`) skips it, and + `Fetching::poll_latch` does not latch onto a warm newest segment. The + pending fetch then marks the track used, the front re-splices, and the + FETCH goes upstream on the new copy (`pending_fetch_follows_a_newer_segment` + already covers that last hop). Rejected: a spliced `resume::Group` that + continues the open warm group, since it is more code on a line `main` already + deleted; and aborting open groups at park, since it breaks a returning + subscriber continuing an open warm group. +- Regression in release `model/origin.rs` tests, modelled on + `returning_reader_continues_an_open_warm_group` with `served_front()`: + subscribe, write two frames into open group 0, read one, drop the + subscriber and wait for the park, then `fetch_group(0, None)`. Expect a new + upstream track request that serves the whole group, and the fetch ends with + every frame. +- The PR targets `release` directly (CONTRIBUTING's backport flow), since + there is nothing to land on `main` first. + +Public API: none. Wire: none. + +## Closes + +- [#5348](https://github.com/moq-dev/moq/issues/5348) - FETCH of a partly delivered group stalls on 0.17.3