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).
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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:
- HTTP/3 requests — the standard HTTP/3 over QUIC framing. An
HTTP/3 request is dispatched through the same axum
Routerash2/http/1.1requests (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. - 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):
- HTTP/3 request streams — standard HTTP/3 GET/POST carrying
:method/:path. These are the same request model ash2/http/1.1, just over HTTP/3 framing. Dispatched through the axumRouter(same router ash2/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. - WebTransport sessions — opened by a browser's
new WebTransport(url)call, which triggers an HTTP/3 extended CONNECT request. The handler accepts the session (thewtransportcrate'sEndpoint::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 viatransport.createBidirectionalStream(); the handler accepts each viasession.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'sDispatcher(see ../call/call-protocol.md forEventEnvelopeand ../call/client-and-adapters.md §"Shared Dispatcher" for theDispatcher— the same dispatch loop theCallAdapteruses foralknet/callconnections, 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
EventEnvelopeJSON 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:
// 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
h3is served by this crate as a first-class transport.
Constraints
h3requires X.509. Browsers don't support RFC 7250 (ADR-027). A node servingh3must have an X.509 identity. Raw-key-only nodes serveh2/http/1.1but noth3.h3is behind theh3feature gate. Thewtransport(or quinn HTTP/3 extension) dependency is heavier thanh2/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
EventEnvelopeis handed to the call protocol'sDispatcher— no SSE translation, no HTTP framing. The browser speaks the call protocol wire format directly. - The HTTP/3 request path uses the same axum
Routerash2/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
wtransportREADME notes the protocol is not yet standardized; the API may change. Theh3feature gate isolates the risk.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
h3/WebTransport is first-class |
ADR-038 | In scope, not deferred; browser streaming uses QUIC streams |
| Browsers require X.509 | ADR-027 | h3 needs X.509 (browser limitation) |
| Browsers are not alknet peers | ADR-034 | Bearer token, no PeerId |
| WebTransport streams → call protocol directly | ADR-012 | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
| Stealth on h3 | ADR-010 | Unknown paths get the decoy |
| HTTP path = operation path (for HTTP/3 requests) | ADR-036 | Same axum Router as h2/http1.1 |
Open Questions
See 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-httpor 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
— the decision that
h3is in scope - ADR-036 —
the HTTP-to-call mapping (the HTTP/3 request path uses the same
axum
Router) - overview.md — crate overview, feature gates
- http-server.md — the
h2/http/1.1companion /workspace/wtransport/— pure-Rust WebTransport reference implementation (theh3feature's candidate dependency)