Files
alknet/docs/architecture/decisions/038-http3-and-webtransport-as-first-class.md
glm-5.2 125cb49cc4 docs(http): defer h3/WebTransport (ADR-044); browsers use WebSocket for v1
Working through the WebTransport implementation path surfaced a scope
question distinct from the hedging-as-deferral anti-pattern ADR-038 was
written to correct. Three findings drove the re-evaluation:

1. The browser bidirectional call-protocol path doesn't require
   WebTransport — WebSocket is full-duplex, EventEnvelope fits a WS
   binary message boundary cleanly, and the Dispatcher is stream-
   agnostic (ADR-012). What WebTransport gives over WebSocket (native
   multi-stream multiplexing, the ALPN-as-stream substrate) benefits the
   proxy use case, not the call protocol.
2. WebTransport is a draft standard (-07, not RFC) on an experimental
   Rust dependency stack (wtransport/h3 both self-describe as not
   production-ready). Either choice puts a draft protocol on the
   security surface of the first release.
3. The ALPN-stream-proxy (ADR-040) is speculative — its WASM parser
   consumers (browser SSH/SFTP/git clients) don't exist yet, and the
   downstream crates WebTransport deferral blocks (SSH, git, SFTP)
   expose their ALPNs natively over QUIC regardless.

This is a scope decision (per ADR-009: a decision that 'genuinely
doesn't need to be made yet because the use case isn't concrete'), not
hedging. The reversal trigger is concrete: a real deployment needing
the ALPN-stream-proxy.

ADR-038 is superseded (its anti-pattern correction stands; its specific
'h3 in scope now' decision is reversed). ADR-040 and ADR-043 are
parked, not superseded — their designs revive unchanged when WebTransport
revives, with §2 (bidirectionality) and §3 (no-PeerId overlay) of ADR-043
transferring to WebSocket for v1.

ADR-044 §5 also states the 'browser is not a peer' rationale that
ADR-034 §4 closed without arguing: peer = addressable node in the
call-protocol peer graph (stable PeerId, PeerRef::Specific-reachable,
identity stable across reconnects), not 'any endpoint that exchanges
calls during a live session.' A browser is the second but not the first
(no stable crypto identity of its own, ephemeral, not addressable from
other nodes). ADR-034 §4 and Assumption 2 are amended by reference.

The wtransport-vs-hyperium dependency question is recorded (not
resolved — WebTransport is deferred) in ADR-044 §'Research note' and
webtransport.md so the revival doesn't re-derive it: wtransport probably
isn't the right choice (axum-bridge friction — it owns its own HTTP
serving path); the hyperium stack (h3 + h3-quinn + h3-webtransport) fits
the axum integration better but its server-side WebTransport API needs
verification before commitment.

Reviewed by architecture-review subagent; all critical cross-reference
issues (ADR-034 §5 stale 'in scope' assertion, ADR-036 Context listing
h3 as implemented, webtransport.md Design Decisions table) resolved.
2026-06-30 05:55:55 +00:00

250 lines
12 KiB
Markdown

# ADR-038: HTTP/3 and WebTransport as First-Class HTTP Transports
## Status
**Superseded by [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
This ADR's *correction* of the "two-way-door-as-deferral" anti-pattern
(ADR-009 §"What this framework is NOT") stands as a document — the
anti-pattern is real, and the reasoning that rejected deferral-as-hedging is
correct. However, this ADR's *specific decision* — that `h3`/WebTransport is
in scope now, not deferred — is reversed by ADR-044. ADR-044 is a
**scope** decision (permitted by ADR-009: "not needed for the current
scope"), not a hedging deferral: the browser bidirectional path uses
WebSocket (RFC 6455, mature, native axum support), the ALPN-stream-proxy
(ADR-040) is the speculative use case whose deferral is the reversal
trigger, and the draft-standard + experimental-deps surface area is not
justified by a concrete v1 requirement.
ADR-040 and ADR-043 are **parked, not superseded** — their designs revive
unchanged when WebTransport revives. See ADR-044 for the full scope rationale,
the reversal trigger, and the research note on the wtransport-vs-hyperium
dependency choice (recorded for the revival so it is not re-derived).
## Context
The alknet-http Phase 0 research findings
(`docs/research/alknet-http/phase-0-findings.md`, decision point DH-2)
framed HTTP/3 + WebTransport as "deferred past v1" — a two-way-door
addition to land "when the agent service needs browser streaming." That
framing was a residual of the "two-way door as deferral" anti-pattern
that ADR-009 §"What this framework is NOT" was later written to prevent.
The deferral framing is rejected here; this ADR records the decision as
made.
WebTransport is not a "later" feature. It is the browser-streaming
transport — QUIC streams are cheap (better than WebSocket/SSE by far for
multiplexed bidirectional streaming), and WebTransport is supported in
major browsers. The `alknet-http` crate's purpose is to be the HTTP
interface library for downstream crates that need to expose HTTP, and
browser streaming is a first-class requirement of that purpose, not a
fast-follow.
### The ALPN registry already reserves h3
The overview ALPN Registry maps `h3` to `HttpAdapter (HTTP/3 +
WebTransport)`. The `h3` ALPN is reserved; the implementation lands as
part of `alknet-http`, not as a future crate. This ADR confirms that
reservation and records the architectural decision: HTTP/3 + WebTransport
is in scope, in this crate, as a first-class transport alongside `h2` and
`http/1.1`.
### Why WebTransport matters
- **QUIC streams are cheap.** A browser opens a WebTransport connection
once and multiplexes many bidirectional streams over it. This is
fundamentally better than WebSocket (one stream, one connection) or
SSE (one-directional, one stream per subscription) for the
subscription-heavy, streaming-heavy patterns the call protocol
supports natively.
- **Browser support.** WebTransport is supported in modern Chromium-based
browsers (Chrome, Edge). The browser path for alknet is WebTransport,
not WebSocket.
- **The call protocol maps cleanly onto WebTransport streams.** A
`call.requested` over a WebTransport bidirectional stream is the same
EventEnvelope framing over a different QUIC stream source. The
`CallConnection`/`Dispatcher` dispatch loop is stream-agnostic
(ADR-012) — the `h3` handler hands a bidirectional stream to the call
protocol the same way the `h2`/`http/1.1` handler hands a hyper
connection to axum.
### The TLS constraint (browsers require X.509)
Browsers do not support RFC 7250 raw public keys (ADR-027, OQ-12). A
WebTransport session from a browser requires an X.509 cert — meaning the
`h3` handler is a domain-hosted-service concern, not a P2P concern. A
node serving WebTransport must have an X.509 identity (`TlsIdentity::X509`
or `TlsIdentity::Acme`). This is a property of the browser, not a
decision this ADR makes — it's recorded so the spec doesn't pretend a
raw-key node can serve browsers.
### WebTransport relay-as-proxy
A distinct WebTransport feature — a proxy that terminates the browser's
WebTransport connection and forwards encrypted traffic to a P2P hub's
Ed25519 endpoint (so the hub need not expose its own public X.509 cert)
— was recorded in ADR-034 §5. That feature does not change the auth model
(bearer token + `PeerEntry.auth_token_hash`; the proxy is transport-only)
and was explicitly placed in the same bucket as the rest of h3/
WebTransport. With this ADR, h3/WebTransport is in scope; the
relay-as-proxy is a genuine scope question (does the proxy belong in
alknet-http or in a separate relay crate?), tracked as OQ-38 — not
deferred, just scoped to a separate decision when the proxy use case
becomes concrete.
## Decision
**HTTP/3 + WebTransport is a first-class HTTP transport in `alknet-http`,
implemented alongside `h2` and `http/1.1`. It is not deferred.**
The `HttpAdapter` implements `ProtocolHandler` for three ALPNs:
| ALPN | Transport | Use case | Browser? |
|------|-----------|----------|----------|
| `http/1.1` | HTTP/1.1 over QUIC stream | Legacy clients, curl | No (browsers use h2/h3) |
| `h2` | HTTP/2 over QUIC stream | Modern HTTP clients, curl | No (browsers use h3) |
| `h3` | HTTP/3 / WebTransport | Browser streaming | Yes (X.509 required) |
All three are served by `alknet-http`. The `h3` ALPN handler upgrades to
WebTransport sessions and serves both HTTP/3 requests (the standard
HTTP/3 over QUIC framing) and WebTransport streams (the bidirectional/
unidirectional stream API). The handler dispatches HTTP/3 requests
through the same axum `Router` as `h2`/`http/1.1`; WebTransport streams
that target the call protocol are handed to the call protocol's dispatch
loop directly (a WebTransport stream is a QUIC bidirectional stream, the
same stream type the call protocol already speaks).
### WebTransport as the browser streaming path
The `h3` handler drives two distinct stream types, distinguished at the
HTTP/3 framing layer (not by peeking application bytes):
1. **HTTP/3 request streams** — standard HTTP/3 GET/POST carrying
`:method`/`:path`. Dispatched through the axum `Router`, same as
`h2`/`http/1.1` (ADR-036). These are not WebTransport streams.
2. **WebTransport sessions** — opened by a browser's
`new WebTransport(url)` call (an HTTP/3 extended CONNECT request).
The handler accepts the session (the `wtransport` crate's
`Endpoint::server` + `accept` + `accept_bi` pattern, or the quinn
HTTP/3 endpoint's WebTransport extension). Within an established
session, the browser creates bidirectional streams via
`transport.createBidirectionalStream()`; the handler accepts each
and dispatches by sub-protocol. For a call-protocol session, the
first frame is an `EventEnvelope` and the handler hands the stream
to the call protocol's `Dispatcher` — no SSE translation, no HTTP
framing. The browser's `WebTransport` JS API speaks to this handler
directly.
The stream-type distinction (HTTP/3 request vs. WebTransport session)
is made at the HTTP/3 frame layer (regular request headers vs. extended
CONNECT), not by reading the first application byte. The first-frame
routing applies *within* a WebTransport session (determining the
sub-protocol), not between an HTTP/3 request and a WebTransport stream.
This means the browser's subscription/streaming path uses WebTransport
streams directly, not the SSE projection (ADR-036) that HTTP/1.1 + HTTP/2
clients use. The same `Subscription` operation is served as SSE over
`h2` and as a native WebTransport stream over `h3` — the projection is
transport-dependent, the operation is the same.
### h3 and the stealth mapping
The `h3` handler participates in the same stealth model as `h2`/
`http/1.1` (ADR-010, ADR-036): a client that offers `h3` gets the HTTP
handler. Unknown WebTransport paths and unknown HTTP/3 paths get the
decoy (configurable). Real services use `alknet/ssh`, `alknet/call`, etc.
### Implementation reference: wtransport
The `wtransport` crate (`/workspace/wtransport/`, v0.7.1) is a pure-Rust
WebTransport implementation built on `quinn` + `h3`/`qpack`. It provides
the `Endpoint::server` + `accept` + `accept_bi` API that the `h3`
handler uses. `wtransport` is a candidate dependency for the `h3`
feature gate; the exact WebTransport library choice (wtransport vs a
quinn-native HTTP/3 + WebTransport extension) is a two-way-door
implementation detail. The one-way constraint is: `h3` is served, by this
crate, as a first-class transport.
### Feature gating
The `h3`/WebTransport support is behind an `h3` feature gate (the
WebTransport/HTTP/3 dependencies are heavier than `h2`/`http/1.1`):
```toml
[features]
default = ["h2", "http1"]
h3 = ["dep:wtransport"] # or the quinn h3 extension
mcp = ["dep:rmcp"] # MCP feature gate (ADR-037)
```
A deployment that only needs `h2`/`http/1.1` (a non-browser-facing
node) does not compile the WebTransport dependencies. A browser-facing
node enables `h3`.
## Consequences
**Positive:**
- Browser streaming uses QUIC streams directly, not SSE-over-HTTP/2.
The call protocol's subscription model maps onto WebTransport streams
with no translation loss — a `call.responded` stream over a
WebTransport bidirectional stream is the native representation.
- The `h3` ALPN reservation in the overview is honored — the
implementation lands in this crate, not a future one.
- A browser-facing node (a hub with an X.509 cert) serves the same
operations over `h3` as a non-browser-facing node serves over `h2`
the operation registry is transport-agnostic, the projection is
transport-dependent.
- The WebTransport relay-as-proxy (ADR-034 §5) has a clear home: it's a
feature that lives in or near `alknet-http`'s `h3` handler, scoped by
OQ-38.
**Negative:**
- `alknet-http` gains the `wtransport` (or equivalent HTTP/3 +
WebTransport) dependency behind the `h3` feature. This is a heavier
dependency than `h2`/`http/1.1`. The feature gate keeps it out of
non-browser-facing builds.
- WebTransport is still a draft standard (the `wtransport` README notes
it). The API may change. This is inherent to being an early adopter
of WebTransport; the `h3` feature gate isolates the risk.
- Browsers require X.509 (ADR-027). A raw-key-only node cannot serve
WebTransport. This is a browser limitation, not an alknet decision,
but it means the `h3` feature is useful only on domain-hosted nodes
with X.509 certs.
## Assumptions
1. **WebTransport is the browser streaming transport for alknet.** The
browser path is WebTransport, not WebSocket and not SSE-over-HTTP/2.
SSE remains the streaming projection for non-WebTransport HTTP
clients (curl, axios over h2); WebTransport is the native path for
browsers.
2. **The `wtransport` crate (or an equivalent quinn-native HTTP/3 +
WebTransport implementation) is the dependency for the `h3` feature.**
The exact library is a two-way-door implementation detail; the
one-way constraint is that `h3` is served by this crate.
3. **The WebTransport relay-as-proxy (ADR-034 §5) is a separate scope
decision (OQ-38).** It is not deferred — it's a feature with a clear
home (the `h3` handler or a sibling relay crate) that gets designed
when the browser-to-P2P-peer proxy use case becomes concrete.
## References
- [ADR-009](009-one-way-door-decision-framework.md) §"What this framework
is NOT" — the anti-pattern this ADR corrects (two-way door as deferral)
- [ADR-010](010-alpn-router-and-endpoint.md) — the ALPN router; `h3` is
one of the ALPNs the `HttpAdapter` registers for
- [ADR-012](012-call-protocol-stream-model.md) — stream-agnostic
correlation; a WebTransport stream is a QUIC bidirectional stream
- [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) — the
browser limitation (no RFC 7250); WebTransport requires X.509
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 (browsers
are not alknet peers) and §5 (WebTransport relay-as-proxy, recorded
for this bucket)
- [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection
for `h2`/`http/1.1` that WebTransport replaces for the browser path
- `docs/research/alknet-http/phase-0-findings.md` DH-2 — the deferral
framing this ADR rejects
- `/workspace/wtransport/` — pure-Rust WebTransport (the `h3` feature's
reference implementation)
- `crates/http/webtransport.md` — the spec that implements the `h3`
handler