Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 38 additions & 1 deletion quest/m1/dropped-sources.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# [M] Consumers see the producer's real error, never Dropped
# [L] Consumers see the producer's real error, never Dropped

## Goal

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

Expand Down Expand Up @@ -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
68 changes: 68 additions & 0 deletions quest/m1/lite-malformed-close.md
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +38 to +40

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 Cover response-side control decoders

The named hooks cover only control streams accepted from the peer, so malformed responses on locally opened streams still bypass this fix. For example, Rust ProbeStream::poll returns the decode error to RecvBandwidth::poll, which logs and swallows it, while JS Subscriber.runProbe likewise catches and logs malformed PROBE reports without closing the session. Include response-side decoders and regression tests, otherwise the stated rule that any control decode error closes the session remains false for publisher-to-subscriber traffic.

AGENTS.md reference: AGENTS.md:L16-L18

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.

Agreed. b68e886 extends the rule to response decoders on locally opened streams (Rust RecvBandwidth/ProbeStream, JS Subscriber.runProbe), with a malformed PROBE report regression.

(Written by Claude Opus 5.5)

`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
47 changes: 47 additions & 0 deletions quest/m1/release-warm-fetch.md
Original file line number Diff line number Diff line change
@@ -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
Loading