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 |
|
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:
- Accepts one bidirectional stream from the QUIC connection
(
connection.accept_bi()→(SendStream, RecvStream)). - Wraps the
(SendStream, RecvStream)pair as a hyperTokioIo-compatible duplex stream — the same byte stream hyper expects for an HTTP connection. - Constructs the axum
Router(built once at adapter construction, cloned per connection — axumRouterisCloneand cheap to clone). - Hands the duplex stream + the axum router to hyper's connection driver
(
hyper::server::conn::http1::Builderorhttp2::Builder::serve_connection), which reads HTTP frames, parses them, dispatches to axum routes, and writes HTTP responses. - 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_openapigateway endpoints (/search,/schema,/call,/batch,/subscribe— ADR-042). These 5 fixed endpoints are the sole invoke path over HTTP. (Route handlers are thegateway-endpointstask; this task wires the router.) GET /healthz(raw route, no auth, no call protocol). (Thehealthz-decoytask; this task wires the route.)GET /openapi.json(serves theto_openapiprojection). (Theto-openapitask; this task wires the route.)- The stealth decoy fallback (unknown paths). (The
healthz-decoytask; this task wires the fallback.) - (Feature-gated)
POST /mcp(theto_mcpstreamable HTTP service). (Theto-mcptask; this task wires the route behind themcpfeature gate.) - Deployment-specific custom routes (ADR-046). The assembly layer
may inject an
axum::Routerof extra routes atHttpAdapterconstruction. (This task implements theextra_routesmerge.)
Custom routes (ADR-046)
Custom routes:
- Are raw HTTP, not call-protocol operations — not registered in the
OperationRegistry, not discoverable via/search, not in theto_openapigateway doc. - May dispatch into the registry via
OperationRegistry::invoke()with a properOperationContext(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
HttpAdapterrouter 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-endpointstask. This task wires the routes into the router and provides the router state. - No
/healthzor decoy logic. Thehealthz-decoytask implements the healthz handler and the decoy fallback. This task wires the routes. - No
/openapi.jsongeneration. Theto-openapitask implements the OpenAPI doc generation. This task wires the route. - No MCP route. The
to-mcptask implements the rmcp service. This task wires the route behind themcpfeature gate. - No WebSocket upgrade handler. The
websocket/upgrade-handlertask 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
HttpAdapterstruct withidentity_provider,registry,decoy,extra_routesDecoyConfigenum withNotFound,StaticSite { root },Redirect { to }HttpAdapter::new(identity_provider, registry)constructorHttpAdapter::with_decoy(self, decoy)builderHttpAdapter::with_extra_routes(self, routes: Router)builder (ADR-046)ProtocolHandler::alpn()returns the configured ALPN (http/1.1orh2)handle()branches onconnection.remote_alpn()for HTTP framinghandle()accepts a QUIC bidirectional stream viaconnection.accept_bi()handle()wraps the stream as a hyperTokioIo-compatible duplexhandle()drives hyper'shttp1::Builderorhttp2::Builder::serve_connection- axum
Routerbuilt once at construction, cloned per connection - Router state holds
Arc<OperationRegistry>+Arc<dyn IdentityProvider> - Custom routes (
extra_routes) merged viaRouter::merge(ADR-046) - Default surface reserved paths take precedence on collision with custom routes
h3ALPN is not registered (deferred per ADR-044)handle()returns when the HTTP connection closes- Unit test:
alpn()returnshttp/1.1orh2per config - Unit test:
DecoyConfig::default()isNotFound - Unit test:
with_extra_routesmerges 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-httpsucceedscargo clippy -p alknet-http --all-targetssucceeds 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.