Skip to content

[Layer 1, review only] Adopt WebTransport session fixes - #1

Open
xenorune wants to merge 11 commits into
masterfrom
adopted/webtransport
Open

xenorune wants to merge 11 commits into
masterfrom
adopted/webtransport

Conversation

@xenorune

@xenorune xenorune commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

What

Layer 1 of the fork stack for runewarp/runewarp#255. This PR targets master for review only and MUST NEVER BE MERGED INTO master. That branch remains an exact mirror of hyperium/h3; later layers build on adopted/webtransport.

Adopt full CONNECT stream IDs, WebTransport draft negotiation, and session close/drain support from spotdemo4/h3. Add regression coverage for GET-before-CONNECT, independent FINs, blocked stream opens, and cancelled lifecycle waiters. Crate names and manifests stay unchanged, and upstream test suites remain enabled.

Why

The accepted carrier decision needs one session alongside ordinary HTTP/3 requests. The fork decision separates adopted fixes from the generic stream-dispatch and WebTransport integration layers.

Stock session IDs incorrectly turn CONNECT stream ID 4 into session ID 1, and stock WebTransport cannot close or drain sessions. Adopted close handling also needed two local corrections: opens blocked on QUIC credit must wake on session termination, and cancelled waiters must release their task wakers without waiting for a session event.

How

Base: hyperium/h3@43745ad87e95b0c035ac9c547aad467303213425. Six adopted commits retain trev's authorship and exact source provenance in their commit messages:

  • spotdemo4/h3@fb114cfa21d79a668dcd99d4354302638dec1458: full CONNECT stream ID, including its regression test.
  • spotdemo4/h3@9564e2eac819dcd72b7e578f38887425a8112776: draft settings/tokens, peer-SETTINGS wait, and SETTINGS buffer sizing.
  • spotdemo4/h3@96c4283ef733757db46ee7009a63a5965ee1d86d: Quinn negotiation tests and client SETTINGS advertisement hooks. These hooks do not implement client sessions or server-stream dispatch.
  • spotdemo4/h3@2a7174a932cf091c4060fe6c73b0a0a86cbff79b: close/drain capsules, session lifecycle, and stream termination.
  • spotdemo4/h3@b0ae8feb6431d4fe1906d7e9ee1ac344f54d5658: wait for the client's CONNECT close.
  • spotdemo4/h3@43d90ad60756ad6d2d5740d2482af6fdb3d35580: immediate FIN, no CONNECT grease, and final close ordering.

Use the negotiation revisions from wt-session-close; wt-drafts contains the equivalent earlier revisions. Skip browser CI, browser-reporting changes, and publishing changes. Skip the superseded delayed-FIN workaround 61646499204706437709876b5c3c18bbcd413a2c; apply the final immediate-FIN/no-grease changes directly on the close-wait fix.

Move adopted network tests into examples/tests, whose existing dependencies avoid all manifest edits. Every async test has an internal timeout. Local fixes cover wire/half-close behavior, close wakeups for credit-blocked opens, and cancellation-owned waiter registrations. No unsafe code is added.

Claude Opus 5.5 reviewed 0e89328c17b1b501f4838cc2293a5f135b379da8. Two subsequent commits address its actionable findings:

  • Separate CONNECT-reader wakeups from stream/open waiters. A counted regression with 50 idle bidi streams and 20 unknown capsules reproduces 1,000 spurious wakeups before the fix and zero afterward. Per-waiter atomic wakers update without scanning or locking the shared set on repeated pending polls; keyed weak entries are removed when the waiter drops. Session termination still wakes every idle stream.
  • Reject bytes after WT_CLOSE_SESSION, whether buffered in the same DATA frame or received in a later frame, using the malformed-capsule H3_MESSAGE_ERROR reset/stop path. Validate peer capsules after a local close too. Session streams terminate immediately on the close capsule; closed() continues through peer FIN and requires a caller timeout if the peer withholds it. Regressions cover both frame cases, repeated error reporting, local-first close, and cancellation followed by another caller completing FIN validation.

The review predates these two fixes; no second review or CI monitoring was performed by this implementation agent.

Final local validation on macOS arm64 with Rust/Cargo 1.96.0:

  • perl -e 'alarm 300; exec @ARGV' cargo test --workspace --all-features: 284 tests and doctests pass, including 24 network tests. Baseline: 247 pass.
  • cargo fmt --all -- --check: pass.
  • cargo clippy --workspace --all-features --all-targets: pass with the same 16 baseline warnings in unchanged upstream tests and examples/client.rs; no new warnings.
  • cargo clippy --workspace --all-features --lib -- -D warnings and cargo clippy -p examples --all-features --tests --example webtransport_server -- -D warnings: pass.
  • ./ci/example_test.sh: passed on the pre-review head; not rerun because these fixes only change WebTransport lifecycle handling.
  • h3spec v0.1.13 native mac-arm64 against the local example server, with the five exclusions from ci/h3spec.sh: 43 cases passed on the pre-review head; not rerun because ordinary HTTP/3 handling is unchanged.
  • Red/green evidence covers the 4 -> 1 regression, premature closed() return, credit-blocked opens that never wake, retained cancelled-waiter wakers, 1,000 unnecessary stream wakeups, and accepted trailing data after peer-first and local-first closes.

Known limits remain explicit. Draft negotiation is not full current-draft conformance; draft 12 flow control and Quinn's missing reset_stream_at support are not implemented. Close/drain sends retain h3's in-flight-write cancellation constraints, and closed() needs an application timeout if the peer never responds. Generic client dispatch, client sessions, exact session validation, invalid CONNECT/cap enforcement, the split server opener/driver, and PATCHES.md belong to later layers. Security PRs hyperium/h3#366, #377, and #378 remain open and unadopted; their dispositions are deferred to the top-layer inventory. Existing encoded-frame/header-memory risks therefore remain. The settings/draft mismatch currently closes the connection; its stream-error/4xx correction stays deferred to layer 3 invalid-CONNECT work. The optional inline WriteBuf size optimization and WebTransportDraft::ALL slice API change are left unchanged to keep this patch focused. Concurrent credit-blocked Quinn bidi opens and received-header exact session validation remain explicit layer 3 work. Browser interop and CI monitoring are outside this implementation handoff.

🤖 Written by @dtcristo's agent (GPT-6.1-Sol, Claude Opus 5.5)

spotdemo4 and others added 11 commits October 24, 2026 20:42
SessionId::from(StreamId) took the stream index (the ID shifted right
by two) while the reverse conversion and decoding from the wire use the
full stream ID. A session on any CONNECT stream other than 0 got the
wrong ID, so streams the server opened for it named a different
session.

(cherry picked from spotdemo4/h3@fb114cf)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
h3 only advertised and recognised SETTINGS_ENABLE_WEBTRANSPORT, the
codepoint of drafts 02 to 06, so clients that only speak newer drafts,
such as Safari 26, could not establish a session (hyperium#347).

Drafts 00 to 16 use four distinct SETTINGS codepoints, each shared by a
range of drafts with the same wire format. WebTransportDraft names those
ranges: Draft02 (02-06), Draft07 (07-11), Draft13 (13-14) and Draft15
(15-16). The server advertises every draft selected with
Builder::webtransport_drafts (all of them by default) and
WebTransportSession::accept uses the latest draft the client also
advertised. Draft 12 shares draft 07's codepoint but requires flow
control, which is not implemented, so SETTINGS_WT_MAX_SESSIONS is always
1 to keep flow control disabled for drafts 13 and 14.

Other changes needed for this:

- WebTransportSession::accept waits for the client's SETTINGS before
  handling the CONNECT request, as the drafts require. Previously a
  CONNECT that arrived before the control stream closed the connection
  with "webtransport is not supported by client".
- The webtransport-h3 upgrade token of drafts 15 and 16 is accepted.
- The sec-webtransport-http3-draft response header is only sent to
  draft 02 clients.
- A SETTINGS frame can hold 16 entries instead of 8, so a peer sending
  every recognised setting is not rejected. The entries now live on the
  heap so that Frame does not grow.
- The control stream header buffer is sized for the largest SETTINGS
  frame. With the new settings, a large max_webtransport_sessions value
  overflowed the 64 byte buffer and panicked.

(cherry picked from spotdemo4/h3@9564e2e)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Adds enable_webtransport and webtransport_drafts to the client builder
so a client can advertise WebTransport drafts, and uses them to check
the draft a server negotiates over a real quinn connection: draft 02 for
Chrome and Firefox, draft 13 for Safari 26, draft 15 for a client using
the webtransport-h3 upgrade token, and the latest draft the server also
supports.

Draft 15 can't be tested against a browser yet, and webtransport-go's
client refuses servers without the reset_stream_at transport parameter,
which quinn does not implement.

The Safari job advertising only drafts 02 and 13 is now informational:
Safari 26 cancels its CONNECT unless draft 07 is advertised.

Adoption: Omit browser CI and manifest edits. Run the Quinn negotiation tests in examples/tests using its existing dependencies, and bound each complete async test with a timeout. The client builder only advertises settings; client session support remains deferred.

(cherry picked from spotdemo4/h3@96c4283)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
WebTransportSession could neither end a session with an error code and
message, nor learn that the client ended one, nor ask the client to
wind down. It now reads and writes the WT_CLOSE_SESSION and
WT_DRAIN_SESSION capsules on the CONNECT stream:

- close(code, reason) sends WT_CLOSE_SESSION and finishes the CONNECT
  stream. The reason is truncated to 1024 bytes at a character boundary.
- drain() sends WT_DRAIN_SESSION.
- closed() waits for the session to terminate and returns its close
  code and reason. A CONNECT stream that ends without a capsule counts as
  code 0 with an empty reason. After a close from the client, the server
  finishes its side of the CONNECT stream. An invalid capsule resets the
  CONNECT stream with H3_MESSAGE_ERROR.
- draining() waits for the client to send WT_DRAIN_SESSION.

Capsules may span DATA frames or share one, and unknown capsules are
skipped without buffering them.

Once a session terminates, which includes being dropped, its streams
are reset and stopped with WT_SESSION_GONE the next time they are used
or when they are dropped, and tasks blocked on them are woken. Opening
new streams fails.

The CONNECT stream is now split, so WebTransportSession requires
C::BidiStream: quic::BidiStream<B>, which h3-quinn satisfies.

Adoption: Keep manifests unchanged. Relocate integration tests and their shared Quinn fixture to examples/tests, which already supplies the required dependencies. Add whole-test async timeouts.

(cherry picked from spotdemo4/h3@2a7174a)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
After close(), closed() returned as soon as the close capsule and FIN
were queued. The example server then dropped the session and with it
the QUIC connection, which discards unsent stream data, so browsers saw
the connection drop instead of the close (Chrome: "Connection lost").

closed() now also waits for the client to finish or reset its side of
the CONNECT stream, as draft 16 recommends before closing the
connection. After a close from the client we stop reading the CONNECT
stream instead, since that close has already arrived.

Adoption: Relocate the regression test under examples/tests and bound the full async test.

(cherry picked from spotdemo4/h3@b0ae8fe)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
The Safari 26 crash on a server close is not caused by the FIN after
the WT_CLOSE_SESSION capsule but by the grease frame that h3's finish()
sent in between: with grease disabled, capsule and FIN together work in
every Safari interop run. Frames of unknown types must be ignored
(RFC 9114, section 9), so this is a Safari bug, but it takes down the
networking of every tab.

close() sends the FIN right after the capsule again, as the drafts
require, and WebTransportSession::accept disables grease on the CONNECT
stream through a new RequestStream::disable_grease() (behind the
i-implement-a-third-party-backend feature). closed() still waits for
the client to close its side before returning.

Adoption: Apply the final no-grease and close-order changes directly on b0ae8fe, omitting the superseded delayed-FIN workaround 6164649. Keep immediate FIN and wait for the client before allowing closed() to finish. Browser CI and example-reporting changes are omitted.

(cherry picked from spotdemo4/h3@43d90ad)

Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
Co-authored-by: GPT-6.1 Sol <noreply@openai.com>
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.

3 participants