Skip to content

fix(net): subscribe blind when moq-transport discovery cannot prove absence - #7187

Closed
zshenker wants to merge 1 commit into
moq-dev:mainfrom
zshenker:fix/ietf-plain-request-subscribes
Closed

zshenker wants to merge 1 commit into
moq-dev:mainfrom
zshenker:fix/ietf-plain-request-subscribes

Conversation

@zshenker

Copy link
Copy Markdown
Contributor

Problem

Since #6445 (and #7173), a plain Origin.Consumer.request(path) on a session whose peer accepted discovery fails Unroutable once that discovery has synced with no route for the path. A moq-transport session reads as synced the moment SUBSCRIBE_NAMESPACE is answered: the protocol marks no end to the initial set, so without MoQ Active Count the subscriber treats REQUEST_OK as the boundary. A relay that accepts SUBSCRIBE_NAMESPACE but serves tracks by origin pull, announcing nothing until a SUBSCRIBE triggers the pull, is therefore unplayable: the request ends before it ever subscribes. Published @moq/net 0.4.5 subscribed directly. #3532 already cites a draft-16 relay that doesn't announce.

The drafts make the direct SUBSCRIBE legal and give no way to prove absence:

  • draft-22 §4: "Given out-of-band information, a subscriber can retrieve tracks from a publisher (including a relay) without any previous MOQT messages besides SETUP."
  • draft-22 §4.1: "A subscriber MAY send SUBSCRIBE, FETCH or TRACK_STATUS for tracks in a namespace without having received a PUBLISH_NAMESPACE for it."
  • draft-22 §4.2: "On success, the publisher MUST send a NAMESPACE message for each namespace it knows ... and further NAMESPACE or NAMESPACE_DONE messages as that set changes." A publisher "knows" only namespaces it originates or received an authorized PUBLISH_NAMESPACE for, and there is no end-of-initial-set signal.
  • draft-18 §6: "Given sufficient out of band information, it is valid for a subscriber to send a SUBSCRIBE or FETCH message to a publisher (including a relay) without any previous MOQT messages besides SETUP."
  • draft-18 §6.2: "A subscriber MAY send SUBSCRIBE or FETCH for tracks in a namespace without having received a PUBLISH_NAMESPACE for it."
  • draft-18 §10.18: "If the SUBSCRIBE_NAMESPACE is successful, the publisher will send matching NAMESPACE messages on the response stream. ... When there are changes ..., the publisher sends the corresponding NAMESPACE or NAMESPACE_DONE messages." No end marker either.

Consumer: @moq/watch with announced="false" (a plain request) against IETF moq-transport relays that don't announce.

Approach

A session now reports internally whether its discovery can prove absence. moq-lite always can (ANNOUNCE_OK's count or SYNCED). moq-transport can only when both sides declare MoQ Active Count. The origin counts those sessions separately. A plain request settles Unroutable on synced absence only when every session proves absence. Otherwise the session that cannot answers it blind, with a direct SUBSCRIBE, exactly as a session without discovery already does. The SUBSCRIBE's own outcome decides from there: a refusal such as does-not-exist ends the track with that error.

  • moq-lite sessions and Active Count peers keep feat(net)!: own broadcast discovery during resolution #6445's Unroutable unchanged.
  • follow() still never takes a blind answer.
  • A later announcement for the path is handled as today: the request moves to the announced route, as it does when a route appears over a blind answer.

Verified in Chrome with @moq/watch against an IETF moq-transport relay that accepts SUBSCRIBE_NAMESPACE and announces nothing until a SUBSCRIBE pulls the track: with announced="false", CMAF plays on drafts 22 and 18; with announced="true", it still waits for an announcement, sending no SUBSCRIBE.

Impact

  • Public API: none. Consumer.discovery keeps its value. Behaviour fix for request() on moq-transport peers without MoQ Active Count.
  • Wire: none.
  • Tests: moqt-18 and moqt-22 integration tests cover a peer that accepts SUBSCRIBE_NAMESPACE and announces nothing. The plain request subscribes and plays, and a refused SUBSCRIBE ends the track with the refusal. A follower never subscribes, and an Active Count peer's empty set still fails Unroutable. A forward test pins the later-announcement switch. The existing feat(net)!: own broadcast discovery during resolution #6445 Unroutable test is unchanged.

Alternatives

  • Treat every moq-transport session as non-discovering: also flips discovery and @moq/watch's announced gate for relays that do announce. Rejected as broader.
  • Wait a grace period after REQUEST_OK before declaring absence: a wall-clock guess that still fails a pull-only relay.

Follow-ups

  • Against a moq-transport relay without Active Count that does announce, a plain request now resolves blind first, then switches to the announced route once its NAMESPACE lands, so a player re-subscribes once. Worth deciding whether a blind answer and a later route from the same session should count as one instance.
  • Rust's moq-transport subscriber also drops the initial-set hold without a count (rs/moq-net/src/ietf/subscriber.rs), so Rust request likely needs the same fix.
  • moq-lite-03/04 mark no initial-set boundary either; check whether their best-effort sync can fail requests early.

Refs #3532, #6445, #7173.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

…bsence

moq-transport marks no end to a SUBSCRIBE_NAMESPACE's initial set unless
both sides declare MoQ Active Count, so its discovery syncing with no route
proves nothing. A plain request covered by such a session took Unroutable
at REQUEST_OK, so a relay that serves by origin pull and announces nothing
was unplayable. Count only sessions that prove absence toward that sync;
the rest answer plain requests with a direct SUBSCRIBE, as one without
discovery does. moq-lite, Active Count peers, and follow() are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zshenker
zshenker marked this pull request as ready for review October 29, 2026 03:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 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 ⚠️ Failed 2026-10-29T03:54:19.876594Z
We couldn’t complete this review. Comment @codex review to try again.
3f371c1 Draft marked ready
ℹ️ 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.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

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)

Reviewed commit: 3f371c1

P2 — Restrict blind answers to the eligible session (js/net/src/origin.ts:927–930, consumed by connection/forward.ts's serveRequests). proven < total enables every session's serving loop, not just the peer that cannot prove absence. Attach a counted/lite session A first and an uncounted moq-transport pull-only session B to one origin, with neither announcing the requested path. A's loop can claim the sole slot.answer first even though only B serves that path; B then skips the occupied slot, and subscribing to A fails instead of pulling through B. This newly supported combination of two discovering peers needs per-session eligibility as well as the aggregate check. Gate blind answering by the session's current proof/downgrade state (preserving discovery-refusal fallback), and add a mixed-session regression that ensures only B consumes the path.

Direction: separating discovery from proof of absence is the right, narrowly scoped approach; the capability must also constrain answer selection. The single-session tests cover the intended behavior and follower isolation, but not this mixed-session case.

Verification: static inspection of the changed accounting, negotiated Active Count, blind answer selection/cleanup, and tests, with independent confirmation of the mixed-session path. I did not run tests or browser/relay interoperability checks.

@kixelated

Copy link
Copy Markdown
Collaborator

Sorry this breaks with multiple connections and CDNs. The JS code was like this but it caused a bunch of issues compared to the Rust code.

Publish a namespace so I know you're a publisher. Don't make me guess and blindly send a SUBSCRIBE to every connection and hope one of them supports it.

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.

2 participants