Files
alkhttp/docs/architecture/decisions/001-alpn-protocol-dispatch.md
T
glm-5.3-flash 320ea87b08 docs: port architecture specs and ADRs from alknet-http; write new alkhttp ADRs 067-070
Phase 1 (SDD) — architecture documentation:

Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint
gateway, channels-over-WS, Sub/Pub operation types):
- overview.md, http-server.md, http-adapters.md, http-mcp.md
- README.md index (rewritten for alkhttp)

New ADRs:
- 067: WebSocket carries the channels protocol (8-byte chunk demux,
  channel 0 = alk/call, upgrade path /alk/channels)
- 068: gateway /publish endpoint for Pub operations (NDJSON body)
- 069: WebTransport out of scope in alkhttp (alknet concern)
- 070: from_wss consumer adapter (wss feature, tokio-tungstenite)

Ported ADRs (25, same numbers, port notes + amendments where the
extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027,
034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066.

websocket.md rewritten for the channels session; open-questions.md
seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing,
OQ-03 from_wss reconnect, OQ-04 browser client ownership).

Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
2026-08-27 14:19:24 +00:00

5.1 KiB

ADR-001: ALPN-Based Protocol Dispatch

Ported from alknet ADR-001 (ALPN-Based Protocol Dispatch); re-targeted to alkhttp.

Status

Accepted

Context

The previous architecture used a three-layer model: transports produced byte streams, interfaces defined how to interpret those streams (StreamInterface, MessageInterface), and OperationEnv dispatched operations through local, irpc, or remote paths. This required a ListenerConfig enum with three variants (Stream, Http, Dns), a server accept loop handling three different listener types, and a complex dispatch model that mixed concerns across layers.

Protocol detection was done by byte-peeking — the server read the first bytes of an incoming connection and guessed which protocol the client was speaking. This is fragile, limits protocol extensibility, and cannot work with encrypted transports where the payload is opaque.

ALPN (Application-Layer Protocol Negotiation) is a TLS extension where the client advertises supported protocols during the handshake and the server selects one. QUIC builds on this natively — every QUIC connection has an ALPN. This is the same pattern iroh uses: Router dispatches incoming QUIC connections to ProtocolHandler implementations based on the ALPN string. Hickory DNS registers ALPN protocols (dot, doq, h2, h3). The reverse-proxy project at @alkdev/reverse-proxy uses the same pattern for TLS.

The core insight: a service IS an ALPN. Every protocol handler registers an ALPN string on a shared endpoint. The ALPN negotiation during the handshake routes the connection to the correct handler before any application bytes are read.

Decision

All protocol dispatch in alkhttp is ALPN-based: the HttpAdapter registers on the standard HTTP ALPNs (h2, http/1.1) on the shared endpoint (the endpoint and TLS/transport layer are alkcall/alknet-side concerns — see the alkcall crate docs). A single endpoint accepts connections, and the ALPN string selected during the handshake determines which ProtocolHandler receives the connection. There is no byte-peeking, no ListenerConfig enum, and no three-layer dispatch model.

The endpoint advertises the union of all registered handlers' ALPN strings. When a client connects, the TLS/QUIC handshake negotiates the ALPN. If the client's offered ALPNs and the server's advertised ALPNs have no intersection, the handshake fails — this is the correct behavior, not an error to work around.

Consequences

Positive:

  • Single dispatch mechanism replaces three separate listener types
  • Protocol detection happens at the TLS layer, not application layer — no byte-peeking
  • Adding a new protocol is registering a new ALPN string — no server code changes
  • Each handler owns its entire wire format — no shared framing layer
  • QUIC connections are cheap — a client that needs multiple protocols opens one connection per ALPN, all multiplexed over the same UDP flow
  • Stealth mode (byte-peek protocol detection on port 443) is unnecessary — ALPN negotiation handles this cleanly
  • WASM story is clean: handlers receive byte streams, protocol parsers that operate on bytes compile to WASM

Negative:

  • ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means alkcall cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-006, ALPN convention and connection model, in the alkcall crate docs).
  • All protocols must be registered at endpoint creation time (or use hot-reload via ArcSwap for dynamic addition)
  • Custom protocols require reserving ALPN strings — we own the alknet/ namespace
  • Debugging requires knowing which ALPN was negotiated (mitigated by logging at the endpoint level)

References

  • Pivot proposal (alknet mono-repo): docs/research/pivot/alpn-service-architecture.md
  • ADR-002: ProtocolHandler trait
  • ADR-003: Crate decomposition
  • iroh reference (alknet mono-repo): docs/research/references/iroh/ (ALPN dispatch, ProtocolHandler pattern)
  • Replaces the old three-layer model (StreamInterface/MessageInterface/OperationEnv)

Port notes

  • Decision retargeted from alknet-wide dispatch to alkhttp: the HttpAdapter registers on the standard HTTP ALPNs (h2, http/1.1); the shared endpoint/TLS-transport layer is an alkcall/alknet-side concern, so "a single QUIC+TLS endpoint" became the transport-agnostic "a single endpoint" (and "shared QUIC+TLS endpoint" → "shared endpoint" in the Context insight).
  • The ADR does not list h3 as an alkhttp deferred registration; the only h3 mention is the Hickory DNS example of ALPN registration, left as-is. h3/WebTransport is an alknet-side concern, not an alkhttp one.
  • ADR-006 reference converted to a textual "alkcall crate docs" reference (the connection/ALPN-convention ADRs are alkcall-internal and not ported here).
  • Mono-repo-relative research paths (docs/research/...) annotated as alknet mono-repo paths.