open-standards/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md
Transaction Science 7e6c0ccc4a vendored wtransport: close a WebTransport session with WT_CLOSE_SESSION
Connection::close_session(code, reason) has the driver's worker write one
DATA frame holding a WT_CLOSE_SESSION capsule (draft-ietf-webtrans-http3-16
§6) on the CONNECT stream at the highest send priority, FIN it, and wait at
most two seconds for the peer to close its side before closing the QUIC
connection with H3_NO_ERROR, so the connection's close cannot overtake the
capsule. The reason is cut to 1024 bytes at a character boundary. After it,
this end opens no stream and sends no datagram; a close asked for just as
the last handle drops still goes out.

A session the peer closes is ConnectionError::SessionClosed (the release
reported it as ApplicationClosed, the variant of the QUIC connection's
close, whose code under HTTP/3 is an HTTP/3 error code), and
Connection::closed() reports the session's end as the driver saw it.

Unit tests of the capsule's bytes against the draft's layout. The
upstream's examples and dev-dependencies are left out so the crate's unit
tests run from the workspace (cargo test -p wtransport --lib). VENDORED.md
lists every change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 08:30:35 -04:00

4.1 KiB

wtransport 0.7.2, vendored

Upstream. The wtransport crate, version 0.7.2, exactly as published on crates.io: the .crate file's SHA-256 is b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036, the checksum the joule-code lock held for it, and .cargo_vcs_info.json names the upstream commit it was packaged from (d022a6ad5f6260cb526e06d8ca42d09516ce6440, directory wtransport). LICENSE-MIT and LICENSE-APACHE are the upstream's, from that commit; the crate is MIT OR Apache-2.0, and these changes are offered under the same terms. The crate's companion, wtransport-proto 0.7.2, is not vendored: nothing in it changes.

Where it is used. [patch.crates-io] in joule-code/joule-code-rs/Cargo.toml and in crates/joule-moq-room/Cargo.toml (a workspace of its own that builds the relay). Its Cargo.lock and Cargo.toml.orig are the upstream's, kept with the source; cargo reads neither, and scripts/check-lockfiles.sh skips the lock.

Why

A MOQT session over WebTransport is closed by closing the WebTransport session (draft-ietf-moq-transport-22 §6.6, which calls the capsule by its earlier name, CLOSE_WEBTRANSPORT_SESSION): a WT_CLOSE_SESSION capsule on the session's CONNECT stream carries a 32-bit application error code and a UTF-8 reason of at most 1024 bytes, followed by a FIN (draft-ietf-webtrans-http3-16 §6). The release reads that capsule but cannot send it: its CONNECT stream belongs to the driver's worker, and Connection::close closes the QUIC connection, whose code a browser's WebTransport API does not expose. A page whose session joule-moq-relay refused saw only that the session was lost.

What changed

  • Connection::close_session(error_code: u32, reason: &str). It hands the driver's worker a request (Driver::close_session); the worker writes one HTTP/3 DATA frame holding the capsule (close_session_capsule, in src/driver/streams/connect.rs) on the CONNECT stream at the highest send priority, sends the FIN, and then reads that stream until the peer's FIN or reset, at most two seconds (SESSION_CLOSE_LINGER), before it closes the QUIC connection with H3_NO_ERROR: §6 asks an endpoint to wait for the peer to close its CONNECT stream first, so the connection's close cannot overtake the capsule. The reason is cut to 1024 bytes at a UTF-8 character boundary, as §6 requires of a sender that truncates.
  • After close_session this end opens no stream and sends no datagram (§6): open_uni, open_bi and send_datagram fail at once.
  • A close requested just as the session's last handle is dropped still goes out: the worker's loop can see the handles gone first, so Worker::run takes a pending request before it ends.
  • ConnectionError::SessionClosed, for a session the peer closed (the capsule's code and reason, or code 0 and no reason for a FIN alone, §6). The release reported that as ApplicationClosed, the variant it also uses for the QUIC connection's close, whose code under HTTP/3 is an HTTP/3 error code; the two could not be told apart.
  • Connection::closed() resolves with how the session ended, as the driver saw it, rather than with how the QUIC connection closed after: SessionClosed with the peer's code and reason, or LocallyClosed as soon as this end's capsule is queued. The release's closed() gave the QUIC close, so a peer's capsule was never reported there.
  • The tokio feature time is named, for the wait's bound (it was on already through quinn).
  • The upstream's examples and dev-dependencies are left out, so that the crate's unit tests, the upstream's and those of the capsule's layout, run from the joule-code workspace: cargo test --locked -p wtransport --lib (cargo tests a non-member crate only when it has no dev-dependencies). The CI job of joule-moq-relay runs them.

What did not change

The streams of a session this end closes are not reset one by one with WT_SESSION_GONE, as §6 asks of an endpoint whose connection carries on; the driver does not track them. They end with the QUIC connection, at most two seconds later. joule-moq-relay serves one session per connection, and the session ends at the capsule for both ends.