Files
alknet/tasks/http/server/http-adapter.md

11 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
http/server/http-adapter Implement HttpAdapter (ProtocolHandler for h2/http1.1) — axum over QUIC stream, ALPN branching, custom routes completed
http/crate-init
http/gateway/gateway-dispatch-spine
broad high component implementation

Description

Implement HttpAdapter in src/server/adapter.rs. This is the ProtocolHandler implementation for the standard HTTP ALPNs (h2, http/1.1) — the highest-risk task in the http crate. It ties together the axum-over-QUIC-stream integration, ALPN branching, the router construction (gateway endpoints + /healthz + /openapi.json + MCP + custom routes + decoy), and the extra_routes: Option<Router> extension point (ADR-046).

The struct (http-server.md §"What")

pub struct HttpAdapter {
    identity_provider: Arc<dyn IdentityProvider>,
    registry: Arc<OperationRegistry>,
    /// The default handler for paths that are not registered operations
    /// (stealth decoy). Configurable: a static site, a fake 404, a
    /// redirect. Two-way-door default (ADR-010).
    decoy: DecoyConfig,
    /// Deployment-specific routes added by the assembly layer (ADR-046).
    /// None = the default surface only. Custom routes are raw HTTP, not
    /// call-protocol operations; they coexist with the default surface and
    /// are not described by to_openapi.
    extra_routes: Option<Router>,
}

pub enum DecoyConfig {
    /// Serve a fake `404 Not Found` (the default — "fake nginx 404").
    NotFound,
    /// Serve a static site from a configured directory.
    StaticSite { root: PathBuf },
    /// Redirect to a configured URL.
    Redirect { to: String },
}

ProtocolHandler impl

#[async_trait]
impl ProtocolHandler for HttpAdapter {
    fn alpn(&self) -> &'static [u8];   // returns the configured ALPN
    async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError>;
}

The HttpAdapter registers for multiple ALPNs (http/1.1, h2). The endpoint's HandlerRegistry maps each ALPN byte string to the same adapter instance; handle() branches on connection.remote_alpn() to pick the HTTP framing. For http/1.1 and h2, the framing is hyper's HTTP/1.1 or HTTP/2 over a QUIC bidirectional stream. WebSocket upgrade layers on top of the same hyper connection driver — a WS upgrade is an HTTP/1.1 or HTTP/2 request that switches protocols (the WS handler is the websocket/upgrade-handler task; this task hosts the route).

Running axum over a QUIC stream (http-server.md §"Running axum over a QUIC stream")

The HttpAdapter::handle() method for h2/http/1.1:

  1. Accepts one bidirectional stream from the QUIC connection (connection.accept_bi()(SendStream, RecvStream)).
  2. Wraps the (SendStream, RecvStream) pair as a hyper TokioIo-compatible duplex stream — the same byte stream hyper expects for an HTTP connection.
  3. Constructs the axum Router (built once at adapter construction, cloned per connection — axum Router is Clone and cheap to clone).
  4. Hands the duplex stream + the axum router to hyper's connection driver (hyper::server::conn::http1::Builder or http2::Builder::serve_connection), which reads HTTP frames, parses them, dispatches to axum routes, and writes HTTP responses.
  5. Returns when the HTTP connection closes (the client disconnects or the stream ends).

The axum Router is built once at adapter construction with the Arc<OperationRegistry> and Arc<dyn IdentityProvider> embedded in its state; cloning the Router per connection clones the Arcs (cheap, shared state), so every request handler has access to the registry and identity provider through the router's state.

The router surface (http-server.md §"Architecture")

The axum Router is the single routing surface for HTTP requests. It contains:

  • The to_openapi gateway endpoints (/search, /schema, /call, /batch, /subscribe — ADR-042). These 5 fixed endpoints are the sole invoke path over HTTP. (Route handlers are the gateway-endpoints task; this task wires the router.)
  • GET /healthz (raw route, no auth, no call protocol). (The healthz-decoy task; this task wires the route.)
  • GET /openapi.json (serves the to_openapi projection). (The to-openapi task; this task wires the route.)
  • The stealth decoy fallback (unknown paths). (The healthz-decoy task; this task wires the fallback.)
  • (Feature-gated) POST /mcp (the to_mcp streamable HTTP service). (The to-mcp task; this task wires the route behind the mcp feature gate.)
  • Deployment-specific custom routes (ADR-046). The assembly layer may inject an axum::Router of extra routes at HttpAdapter construction. (This task implements the extra_routes merge.)

Custom routes (ADR-046)

Custom routes:

  • Are raw HTTP, not call-protocol operations — not registered in the OperationRegistry, not discoverable via /search, not in the to_openapi gateway doc.
  • May dispatch into the registry via OperationRegistry::invoke() with a proper OperationContext (caller identity from the resolved bearer token) — the OAI proxy pattern. Or they may be pure HTTP (a webhook receiver, a static asset server) that never touches the registry.
  • Run under the default Bearer-auth middleware; a route that wants different auth applies its own axum middleware (the deployment owns its custom routes' middleware stack).
  • Do not collide with the reserved default-surface paths (/search, /schema, /call, /batch, /subscribe, /healthz, /openapi.json, the MCP route) — the default surface wins on collision; custom routes namespace away naturally (/v1/...).
  • Are not versioned by to_openapi (ADR-045 versions the gateway contract, not custom routes).
  • Are immutable after construction (matches OQ-04 / ADR-010's static-registration constraint; the HttpAdapter router is built once at startup).

The extension point is additive: a deployment that passes None gets exactly the default surface. The mechanism (the constructor parameter) is the one-way door — once downstream deployments build against it, it's a contract (ADR-046). The specific routes a deployment adds are a two-way door (add/remove freely).

ALPN branching

The HttpAdapter registers for http/1.1 and h2. The endpoint's HandlerRegistry maps each ALPN to the same HttpAdapter instance; the handler branches on connection.remote_alpn() to pick the right framing. The h3 ALPN is not registered in v1 (deferred per ADR-044).

What this task does NOT do

  • No gateway route handlers. The 5 gateway endpoints' handler logic is the gateway-endpoints task. This task wires the routes into the router and provides the router state.
  • No /healthz or decoy logic. The healthz-decoy task implements the healthz handler and the decoy fallback. This task wires the routes.
  • No /openapi.json generation. The to-openapi task implements the OpenAPI doc generation. This task wires the route.
  • No MCP route. The to-mcp task implements the rmcp service. This task wires the route behind the mcp feature gate.
  • No WebSocket upgrade handler. The websocket/upgrade-handler task implements the WS upgrade. This task hosts the WS upgrade route on the router (the WS task depends on this task's router).

Acceptance Criteria

  • HttpAdapter struct with identity_provider, registry, decoy, extra_routes
  • DecoyConfig enum with NotFound, StaticSite { root }, Redirect { to }
  • HttpAdapter::new(identity_provider, registry) constructor
  • HttpAdapter::with_decoy(self, decoy) builder
  • HttpAdapter::with_extra_routes(self, routes: Router) builder (ADR-046)
  • ProtocolHandler::alpn() returns the configured ALPN (http/1.1 or h2)
  • handle() branches on connection.remote_alpn() for HTTP framing
  • handle() accepts a QUIC bidirectional stream via connection.accept_bi()
  • handle() wraps the stream as a hyper TokioIo-compatible duplex
  • handle() drives hyper's http1::Builder or http2::Builder::serve_connection
  • axum Router built once at construction, cloned per connection
  • Router state holds Arc<OperationRegistry> + Arc<dyn IdentityProvider>
  • Custom routes (extra_routes) merged via Router::merge (ADR-046)
  • Default surface reserved paths take precedence on collision with custom routes
  • h3 ALPN is not registered (deferred per ADR-044)
  • handle() returns when the HTTP connection closes
  • Unit test: alpn() returns http/1.1 or h2 per config
  • Unit test: DecoyConfig::default() is NotFound
  • Unit test: with_extra_routes merges routes without collision on reserved paths
  • Integration test: handle() serves an HTTP request over a mock QUIC stream
  • Integration test: custom route (/v1/foo) coexists with default surface
  • Integration test: reserved path (/healthz) wins over a custom route collision
  • cargo test -p alknet-http succeeds
  • cargo clippy -p alknet-http --all-targets succeeds with no warnings

References

  • docs/architecture/crates/http/http-server.md — HttpAdapter, axum over QUIC, custom routes
  • docs/architecture/decisions/010-alpn-router-and-endpoint.md — ADR-010 (stealth, ALPN dispatch)
  • docs/architecture/decisions/046-assembly-layer-custom-http-routes.md — ADR-046 (extra_routes)
  • docs/architecture/decisions/044-defer-webtransport-browsers-use-websocket.md — ADR-044 (h3 deferred)
  • docs/architecture/decisions/039-http-server-and-client-host-colocated.md — ADR-039 (one crate)

Notes

This is the highest-risk task in the http crate — the axum-over-QUIC integration is the merge point. The router is built once at construction with the registry and identity provider in its state; cloning per connection is cheap (Arc clone). The extra_routes extension point (ADR-046) is additive: None = default surface only; Some(routes) = default + custom, with default winning on collision. The h3 ALPN is not registered (deferred per ADR-044). This task wires the router and provides the QUIC-to-hyper bridge; the gateway endpoints, healthz, decoy, openapi.json, MCP, and WS upgrade route are wired by their respective tasks (which depend on this task's router).

Summary

Implemented HttpAdapter (ProtocolHandler for h2/http1.1) in src/server/adapter.rs. Axum-over-QUIC bridge via hyper-util auto Builder. DecoyConfig enum (NotFound/StaticSite/Redirect). with_extra_routes merge (ADR-046). Router state holds Arc + Arc. Placeholder 501 handlers for gateway endpoints/healthz/openapi.json/MCP (later tasks fill in). h3 ALPN not registered (ADR-044). 78 tests pass. Clippy clean on default + no-default-features.