From 5e7f455ce4212f03929db306c79f409a7c02b660 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 05:34:35 -0700 Subject: [PATCH 1/2] chore(quest): fix the WebTransport close capsule upstream and drop CLOSE_LINGER Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/wt-close-upstream.md | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 quest/m1/wt-close-upstream.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 63cb0a6250..641d047c02 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -29,6 +29,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Graceful close in bindings](/quest/m1/bindings-graceful-close.md) - on dev, `shutdown` drains a session in moq-ffi and every wrapper like Rust, so the wrappers keep the tail of a publish - [Close codes](/quest/m1/close-codes.md) - a client sees the peer's application close code over WebSocket and raw QUIC, like WebTransport - [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one +- [WebTransport close upstream](/quest/m1/wt-close-upstream.md) - web-transport-moq delivers the close capsule itself, and moq-tokio's `CLOSE_LINGER` is deleted - [Live in apps](/quest/m1/announce-live-apps.md) - the demo and `@moq/room` show "no broadcasts" from the `live` marker, which waits for the first session on page load - [IETF hidden default](/quest/m1/ietf-hidden-default.md) - a moq-transport peer without the MoQ Hidden option is advertised hidden namespaces; one with it filters per subscription - [Watch refusal](/quest/m1/watch-refusal.md) - `` shows an origin refusal as an error instead of sitting offline diff --git a/quest/m1/wt-close-upstream.md b/quest/m1/wt-close-upstream.md new file mode 100644 index 0000000000..5f85fd98ef --- /dev/null +++ b/quest/m1/wt-close-upstream.md @@ -0,0 +1,38 @@ +# [S] WebTransport close delivers its capsule upstream + +## Goal + +Closing a WebTransport session delivers CLOSE_WEBTRANSPORT_SESSION, so a +browser sees the close code and reason, even when the caller drops its last +session handle right after `close()`. The fix lives in `web-transport-moq`, +and moq-tokio's `CLOSE_LINGER` workaround is deleted. + +## Plan + +[#4429](https://github.com/moq-dev/moq/pull/4429) found the root cause: +`web-transport-moq` 1.3.2's `Session::close` sends the capsule from a spawned +task that holds the connection and the CONNECT stream, but not the H3 +`Settings` (control and QPACK streams). Dropping the last `Session` right +after `close()` finishes the control stream in the same flight as the +capsule, and Chromium reports "Connection lost." with no code. #4429 worked +around it in `rs/moq-tokio/src/transport.rs` by keeping a session clone in a +detached task until `closed()` resolves, capped at a 10s `CLOSE_LINGER`. + +Decided 2026-09-29: fix it at the source and remove the timeout workaround. + +- In moq-dev/noq, the close path keeps whatever the capsule needs alive until + it is delivered, without the caller's help. Add a regression test there + that drops the session right after `close()`. +- Release `web-transport-moq` 1.3.x and bump the pin on `main` (2.x on `dev` + if it has moved). +- Delete `CLOSE_LINGER`, its task, and its mock-session tests from + moq-tokio. #4429 has no end-to-end test, so add a moq-tokio regression + where a refused `https://` session reports its code to the client and + fails on the 1.3.2 pin without the linger. + +Public API: none. Wire: none. + +## Related + +- [Close codes on every transport](/quest/m1/close-codes.md) - the same symptom over qmux and raw QUIC +- [UnknownSession log flood](/quest/m1/unknown-session-logs.md) - another `web-transport-moq` release and pin bump From a836d6a0875cd510a9d09c23c7b6c12726963d55 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 05:40:24 -0700 Subject: [PATCH 2/2] chore(quest): prove the WebTransport close fix at the layer that fails Co-Authored-By: Claude Opus 5.5 --- quest/m1/wt-close-upstream.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/quest/m1/wt-close-upstream.md b/quest/m1/wt-close-upstream.md index 5f85fd98ef..a277c17cb0 100644 --- a/quest/m1/wt-close-upstream.md +++ b/quest/m1/wt-close-upstream.md @@ -21,14 +21,17 @@ detached task until `closed()` resolves, capped at a 10s `CLOSE_LINGER`. Decided 2026-09-29: fix it at the source and remove the timeout workaround. - In moq-dev/noq, the close path keeps whatever the capsule needs alive until - it is delivered, without the caller's help. Add a regression test there - that drops the session right after `close()`. + it is delivered, without the caller's help. Its regression test drops the + session right after `close()` and asserts the peer reads the capsule + before the H3 control stream ends; it must fail on 1.3.2. A Rust peer + alone proves nothing: `session_close_surfaces_a_rejection_code` in + `rs/moq-tokio/tests/broadcast.rs` passed on 1.3.2 without the linger, + because only Chromium treats the control stream's end as fatal. Add a + browser check too if the playwright harness makes it cheap. - Release `web-transport-moq` 1.3.x and bump the pin on `main` (2.x on `dev` if it has moved). - Delete `CLOSE_LINGER`, its task, and its mock-session tests from - moq-tokio. #4429 has no end-to-end test, so add a moq-tokio regression - where a refused `https://` session reports its code to the client and - fails on the 1.3.2 pin without the linger. + moq-tokio. Public API: none. Wire: none.