joule-moq-relay: a WebTransport session closes with its MOQT code and reason (WT_CLOSE_SESSION), on a vendored wtransport 0.7.2 #365
Loading…
Reference in a new issue
No description provided.
Delete branch "joule-moq/wt-session-close"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
A MOQT session over WebTransport is now closed by closing the WebTransport session
(
draft-ietf-moq-transport-22§6.6), not the QUIC connection under it. The relay sends aWT_CLOSE_SESSIONcapsule (0x2843) and a FIN on the session's CONNECT stream(
draft-ietf-webtrans-http3-16§6). The capsule carries two things:A page now reads the relay's code and reason over WebTransport exactly as it does over the
WebSocket binding. For example, a grant presented without its proof gives
UNAUTHORIZED(2)with
live-rooms §10.5: pop_missing. Before this change the page saw onlyConnection lost.The release of
wtransport(0.7.2, the newest) reads that capsule but cannot send it. Its CONNECTstream belongs to the driver's worker, and
Connection::closecloses the whole QUIC connection.So this PR vendors wtransport 0.7.2 and adds a session close to it.
The vendored crate:
joule-code/joule-code-rs/vendor/wtransport09700dfdfis the crate exactly as published. Its.crateSHA-256 isb4273ce3…, the checksum the lock held. The commit adds the upstream'sLICENSE-MITandLICENSE-APACHE, taken from the commit that.cargo_vcs_info.jsonnames.[patch.crates-io]is set injoule-code-rs/Cargo.tomland incrates/joule-moq-room/Cargo.toml(a workspace of its own that builds the relay). In both locksthe only change is that the wtransport entry loses its registry
sourceandchecksumlines.scripts/check-lockfiles.shskips the vendored crate's own lock, as italready skips the other vendored tree's.
7e6c0ccc4andc357d5d12;VENDORED.mdlists every change and why.Connection::close_session(code: u32, reason: &str). The worker writes one DATA frameholding the capsule on the CONNECT stream, at the highest send priority, and sends a FIN. It
then waits at most 2 s for the peer to close its side of that stream. Only after that does it
close the QUIC connection, with
H3_NO_ERROR. §6 asks for this wait so that the connection'sclose cannot overtake the capsule.
that truncates.
ConnectionError::SessionClosedreports a session the peer closed. The release reportedthat case as
ApplicationClosed, the same variant it uses for the QUIC connection's close,whose code under HTTP/3 is an HTTP/3 error code, so the two could not be told apart.
Connection::closed()reports how the session ended, as the driver saw it, instead of howthe QUIC connection closed afterwards.
cargo test --locked -p wtransport --libruns from the workspace. That command runs the upstream's unit tests and the new ones,including the capsule's bytes checked against the draft's layout. The relay's CI job now runs
it.
wtransport-protois not vendored, because nothing in it changes.The relay
WtConnection::closecallsclose_session.u32. Session codes are not put throughwt_to_h3: thatmapping is for stream resets (§4.4), while session codes travel unmapped in the capsule.
SessionClosedasClosed { code, reason, by_peer: true }. This covers the relay's own client and the relay'ssessions alike. As a result, a page's close code now reaches the session's signed summary.
code: u64::MAX, the relay's existing"no MOQT code" value.
is held until the QUIC connection has closed (
Closingin the binding, bounded at 10 s). Withoutthis, the stream's FIN or STOP_SENDING would go out at once and reach the page before the capsule.
read_setuplikewise keeps a stream it refuses inearly, so that stream is also dropped onlyafter the close.
The browser finding behind the stream hold
The initial browser run of this change hung at the bare-grant step. Alice's page had lost its
renderer ("Target crashed"), about 10 ms after the relay refused the session.
Reproduction. I reproduced it on loopback in a small harness, with a page and the relay
binary in the host's browser. The page opened a stream, and the relay refused that stream and
dropped it before writing the capsule. The page lost its renderer in most runs; a page that sent
nothing never did.
Fix. With the relay holding the streams until the QUIC close, the same harness passed 6 of 6,
and both full browser runs pass. In a second probe the relay closed every page at a 7 s session
limit while all of them were publishing. Every page read
(0, "the relay's session limit of 7 s"),and no renderer was lost.
Residual case. One case still loses the renderer. A page that FINs a stream which the relay
refuses within the same instant can still hit it, because the page's own FIN completes after the
close. This failure is inside the browser and happens only for a page that breaks a rule. The
relay cannot order the page's own stream events.
Design decisions
CONNECT streams have been closed by the peer" before CONNECTION_CLOSE. The wait is bounded at
2 s.
reason: &str, not&[u8]. §6 requires UTF-8, so the type enforces it. The relay passesfrom_utf8_lossyof its reason bytes.ApplicationClosed. The tests rely on this: withone variant, a QUIC close that carries code 2 looks exactly like a session closed with 2, and
the old close would pass them.
quinn offers no reset by stream ID. The streams end with the QUIC connection instead. This is
enough because the relay serves one session per connection.
the stream wrappers, so the hold costs nothing on the media path: a stream is boxed only when it
is dropped during a close.
joule-code/joule-code-rs/vendor/**.origin/mainatd9a614dd, which includes #351 (DVR), #358 and #360. The test setsthe grant rule's new
seekfield, and the changelog entry sits above DVR's. Both locks werere-validated.
Checks (on the build host through
rb, unless marked local; all on the merged tree)tests/dvr.rsincluded)cargo test --locked -p joule-moq-relay --all-targets--test webtransport_close, 10 runscargo test --locked -p wtransport --libcargo test --locked -p joule-moq-ingest --all-targetscd crates/joule-moq-room && cargo test --locked --all-targetscargo clippy --locked -p joule-moq-relay --all-targets;cargo clippy --locked -p wtransport --lib --testsserver.rs:167andsession.rs:311are on main already)cargo metadata --locked --offlinein both workspaces;scripts/check-lockfiles.shon bothBROWSER_BIN=… MOQ_WEB_OUT=…/wt-close-e2e/full1 e2e/run.sh --runs 2…/full2Falsifications
All mutations ran on committed code, each reverted after its run.
F1: the old
WtConnection::close(the QUIC connection's close). Each check oftests/webtransport_close.rsfails on its own; for each run, the checks before it were madenon-fatal.
u64::MAXwith "the connection closed without closing thesession (HTTP/3 error 0x2): live-rooms §10.5: pop_missing".
[("webtransport", None)].u64::MAXwith "(HTTP/3 error 0x0): the relay's sessionlimit of 3 s".
F2: the capsule's code written little-endian. 3 capsule unit tests fail.
F3: the 1024-byte cut without the character-boundary check. The truncation test fails.
F4: the old
Connection::closed(). The client read(0, "closed by this endpoint", by_peer: false).F5a: the binding holds nothing. The new ordering check fails: "the relay stopped the stream
ahead of the session's close:
Ok(Some(0))".F5b:
read_setupdrops the refused stream before the close. The same check fails the sameway.
Browser: the old close,
--only webtransport. The flipped check FAILs. The page read{"code":-1,"reason":"Connection lost."}; 30 of 31 checks were ok.What is not done
instant.
VENDORED.mdlists what tocarry forward.
🤖 Generated with Claude Code