docs(http): draft alknet-http architecture specs and ADRs 036-039
First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3 server + from_openapi/to_openapi/from_mcp/to_mcp adapters). Specs (crates/http/): - README.md, overview.md — crate index, two-roles-in-one-crate framing, adapter location map, feature gates (h3, mcp), no-env-vars invariant - http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream, Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy - http-adapters.md — from_openapi (reqwest) and to_openapi (projection), error fidelity (HTTP_<status> per ADR-023), type definitions - http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only - webtransport.md — h3/WebTransport handler, browser streaming path, HTTP/3 request vs WebTransport session distinguished at framing layer ADRs: - ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path mapping; to_openapi is projection, not router (the load-bearing one-way door from Phase 0 DH-3) - ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP only; stdio is not built (RCE-vector security position) - ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in scope, not deferred, per ADR-009 §'What this framework is NOT' - ADR-039 HTTP Server and Client Host Colocated in alknet-http (Proposed) — one crate for server + client host (shared HTTP deps, shared operation-spec->HTTP mapping) - ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation crate (the alknet-http -> alknet-call dependency edge) Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'): - OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not a deferral — the decision is made when the use case becomes concrete) - OQ-39 to_openapi published-spec versioning (one-way after first publication) - OQ-40 reqwest client config and connection pooling (two-way-door) Architecture README and overview updated with doc table, ADR table (036-039), current-state note, and crate graph (alknet-http -> alknet-call edge). Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5 suggestion issues found and fixed (missing ADR-039, WebTransport stream routing conflation, undefined types, stale OQ-37 deferral language, README OQ table completeness, Bearer-only attribution, cross-references, ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
This commit is contained in:
232
docs/architecture/crates/http/webtransport.md
Normal file
232
docs/architecture/crates/http/webtransport.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-29
|
||||
---
|
||||
|
||||
# WebTransport — the h3 ALPN handler
|
||||
|
||||
The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
|
||||
WebTransport. This document covers the WebTransport session/stream
|
||||
handling, the browser streaming path, and the relationship to the `h2`/
|
||||
`http/1.1` server. The `h3` support is a first-class transport, not a
|
||||
deferral (ADR-038).
|
||||
|
||||
## What
|
||||
|
||||
The `h3` ALPN handler is the same `HttpAdapter` instance that serves
|
||||
`h2`/`http/1.1`, registered for the `h3` ALPN when the `h3` feature is
|
||||
enabled. It serves two things on a single `h3` connection:
|
||||
|
||||
1. **HTTP/3 requests** — the standard HTTP/3 over QUIC framing. An
|
||||
HTTP/3 request is dispatched through the same axum `Router` as `h2`/
|
||||
`http/1.1` requests (ADR-036 — the HTTP path IS the operation path).
|
||||
From the axum router's perspective, an HTTP/3 request is just
|
||||
another HTTP request; the framing difference is handled below the
|
||||
router.
|
||||
2. **WebTransport sessions** — the browser streaming path. A WebTransport
|
||||
session is a long-lived connection over which the browser opens
|
||||
bidirectional and unidirectional streams. A WebTransport stream that
|
||||
targets the call protocol is handed to the call protocol's dispatch
|
||||
loop directly — a WebTransport bidirectional stream is a QUIC
|
||||
bidirectional stream, the same stream type the call protocol already
|
||||
speaks (ADR-012).
|
||||
|
||||
### Why h3 is a first-class transport
|
||||
|
||||
WebTransport is the browser streaming transport. QUIC streams are cheap
|
||||
(multiplexed over one connection, no head-of-line blocking), and
|
||||
WebTransport is supported in major browsers. The call protocol's
|
||||
subscription/streaming model maps onto WebTransport streams with no
|
||||
translation loss — a `call.responded` stream over a WebTransport
|
||||
bidirectional stream is the native representation, not an SSE
|
||||
translation (which is the projection for `h2`/`http/1.1` clients per
|
||||
ADR-036).
|
||||
|
||||
The Phase 0 research framing ("defer h3/WebTransport past v1") was a
|
||||
residual of the "two-way door as deferral" anti-pattern (ADR-009 §"What
|
||||
this framework is NOT"). WebTransport is in scope, in this crate, as a
|
||||
first-class transport. See ADR-038.
|
||||
|
||||
## Architecture
|
||||
|
||||
### The h3 handler entry
|
||||
|
||||
The `HttpAdapter::handle()` method for the `h3` ALPN 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`. These are the same request model as `h2`/
|
||||
`http/1.1`, just over HTTP/3 framing. Dispatched through the axum
|
||||
`Router` (same router as `h2`/`http/1.1`, ADR-036). An HTTP/3 request
|
||||
is never a WebTransport stream — the stream type is set by the
|
||||
HTTP/3 frame that opens it.
|
||||
2. **WebTransport sessions** — opened by a browser's
|
||||
`new WebTransport(url)` call, which triggers an HTTP/3 extended
|
||||
CONNECT request. The handler accepts the session (the `wtransport`
|
||||
crate's `Endpoint::server(config)?.accept().await.await?.accept()
|
||||
.await?` pattern, or the quinn HTTP/3 endpoint's WebTransport
|
||||
extension — the exact library is a two-way-door implementation
|
||||
detail, ADR-038). Within an established session, the browser opens
|
||||
bidirectional streams via `transport.createBidirectionalStream()`;
|
||||
the handler accepts each via `session.accept_bi()`.
|
||||
|
||||
The two stream types are not disambiguated by "reading the first frame"
|
||||
— they are distinguished by the HTTP/3 frame type that opens them
|
||||
(regular request headers vs. extended CONNECT). The "first frame"
|
||||
routing below applies *within* a WebTransport session, not between an
|
||||
HTTP/3 request and a WebTransport stream.
|
||||
|
||||
### WebTransport session and stream handling
|
||||
|
||||
Once a WebTransport session is established (via extended CONNECT), the
|
||||
browser creates bidirectional streams within it. The handler accepts
|
||||
each stream (`session.accept_bi()`) and reads the first frame to
|
||||
determine the sub-protocol:
|
||||
|
||||
- **Call-protocol `EventEnvelope`** — the stream is a call-protocol
|
||||
stream. The handler hands the `(SendStream, RecvStream)` pair to the
|
||||
call protocol's `Dispatcher` (see [../call/call-protocol.md](../call/call-protocol.md)
|
||||
for `EventEnvelope` and [../call/client-and-adapters.md](../call/client-and-adapters.md)
|
||||
§"Shared Dispatcher" for the `Dispatcher` — the same dispatch loop
|
||||
the `CallAdapter` uses for `alknet/call` connections, ADR-012,
|
||||
stream-agnostic correlation). The browser speaks the EventEnvelope
|
||||
wire format directly over the WebTransport stream.
|
||||
- **Other sub-protocols** — a session may carry other framing
|
||||
conventions (e.g., a future WT-native RPC framing). The session's
|
||||
purpose is declared at CONNECT time (by path/origin), so the handler
|
||||
knows which sub-protocol to expect; the first-frame tag is a
|
||||
belt-and-suspenders disambiguator for sessions that multiplex
|
||||
sub-protocols. For the call-protocol session, the first frame is an
|
||||
`EventEnvelope` JSON object; the handler dispatches accordingly.
|
||||
|
||||
The browser's `WebTransport` JS API is the client side of this:
|
||||
`new WebTransport('https://hub.example.com')` →
|
||||
`transport.createBidirectionalStream()` → write an `EventEnvelope` frame
|
||||
→ read `call.responded` frames. No SSE translation, no HTTP framing —
|
||||
the call protocol speaks directly over the WebTransport stream.
|
||||
|
||||
### Subscription projection (native, not SSE)
|
||||
|
||||
A `Subscription` operation served over WebTransport projects its
|
||||
`call.responded` stream directly onto the WebTransport bidirectional
|
||||
stream — each `call.responded` event is a frame on the stream, no SSE
|
||||
`data:` framing. `call.completed` closes the stream; `call.aborted`
|
||||
closes the stream with an error frame. This is the native streaming
|
||||
projection; SSE (ADR-036) is the projection for `h2`/`http/1.1` clients
|
||||
that don't speak WebTransport.
|
||||
|
||||
### 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 — 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 spec makes. It's
|
||||
recorded so the spec doesn't pretend a raw-key node can serve browsers.
|
||||
A raw-key node serves `h2`/`http/1.1` (for curl, axios, alknet-native
|
||||
clients) but not `h3`/WebTransport (for browsers). A browser-facing hub
|
||||
has a `PeerEntry` with mixed fingerprints (Ed25519 for P2P, X.509 for
|
||||
browsers — ADR-030, ADR-034 §3).
|
||||
|
||||
### Browsers are not alknet peers
|
||||
|
||||
A browser connecting to a hub over WebTransport is served by the `h3`
|
||||
handler. The browser authenticates by bearer token (HTTP `Authorization`
|
||||
header on the WebTransport session request), resolved by the hub's
|
||||
`IdentityProvider::resolve_from_token` against the hub's
|
||||
`PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not** an
|
||||
alknet peer (ADR-034 §4): it gets no `PeerId`, does not enter
|
||||
`PeerCompositeEnv`, and its "ops" are WebTransport streams served by
|
||||
the `h3` handler, not entries in the call-protocol peer-keyed overlay.
|
||||
|
||||
### Stealth on h3
|
||||
|
||||
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 (the same configurable `DecoyConfig` — fake 404, static site,
|
||||
redirect). 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`. Its API:
|
||||
|
||||
```rust
|
||||
// Server (from the wtransport README):
|
||||
let config = ServerConfig::builder()
|
||||
.with_bind_default(4433)
|
||||
.with_identity(&identity) // X.509 identity
|
||||
.build();
|
||||
let connection = Endpoint::server(config)?
|
||||
.accept().await // await connection
|
||||
.await? // await session request
|
||||
.accept().await?; // await ready session
|
||||
let stream = connection.accept_bi().await?;
|
||||
```
|
||||
|
||||
`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
|
||||
(ADR-038); the one-way constraint is that `h3` is served by this crate
|
||||
as a first-class transport.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **`h3` requires X.509.** Browsers don't support RFC 7250 (ADR-027).
|
||||
A node serving `h3` must have an X.509 identity. Raw-key-only nodes
|
||||
serve `h2`/`http/1.1` but not `h3`.
|
||||
- **`h3` is behind the `h3` feature gate.** The `wtransport` (or
|
||||
quinn HTTP/3 extension) dependency is heavier than `h2`/`http/1.1`;
|
||||
non-browser-facing deployments don't compile it.
|
||||
- **Browsers are not alknet peers.** A browser over WebTransport
|
||||
authenticates by bearer token, gets no `PeerId` (ADR-034 §4).
|
||||
- **WebTransport streams target the call protocol directly.** A
|
||||
WebTransport bidirectional stream carrying an `EventEnvelope` is
|
||||
handed to the call protocol's `Dispatcher` — no SSE translation, no
|
||||
HTTP framing. The browser speaks the call protocol wire format
|
||||
directly.
|
||||
- **The HTTP/3 request path uses the same axum `Router` as `h2`/
|
||||
`http/1.1`.** An HTTP/3 request is just another HTTP request from
|
||||
the router's perspective (ADR-036).
|
||||
- **WebTransport is a draft standard.** The `wtransport` README notes
|
||||
the protocol is not yet standardized; the API may change. The `h3`
|
||||
feature gate isolates the risk.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams |
|
||||
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) |
|
||||
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` |
|
||||
| WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
|
||||
| Stealth on h3 | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Unknown paths get the decoy |
|
||||
| HTTP path = operation path (for HTTP/3 requests) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | Same axum `Router` as h2/http1.1 |
|
||||
|
||||
## Open Questions
|
||||
|
||||
See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- **OQ-38** (open, scope): WebTransport relay-as-proxy — a proxy that
|
||||
terminates the browser's WebTransport connection and forwards to a
|
||||
P2P hub's Ed25519 endpoint (so the hub need not expose a public
|
||||
X.509 cert). Recorded in ADR-034 §5. Does the proxy live in
|
||||
`alknet-http` or a separate relay crate? This is a genuine scope
|
||||
question (the proxy use case is not yet concrete enough to decide the
|
||||
crate boundary), not a deferral.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md)
|
||||
— the decision that `h3` is in scope
|
||||
- [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) —
|
||||
the HTTP-to-call mapping (the HTTP/3 request path uses the same
|
||||
axum `Router`)
|
||||
- [overview.md](overview.md) — crate overview, feature gates
|
||||
- [http-server.md](http-server.md) — the `h2`/`http/1.1` companion
|
||||
- `/workspace/wtransport/` — pure-Rust WebTransport reference
|
||||
implementation (the `h3` feature's candidate dependency)
|
||||
Reference in New Issue
Block a user