222 lines
11 KiB
Markdown
222 lines
11 KiB
Markdown
---
|
|
id: http/server/http-adapter
|
|
name: Implement HttpAdapter (ProtocolHandler for h2/http1.1) — axum over QUIC stream, ALPN branching, custom routes
|
|
status: completed
|
|
depends_on: [http/crate-init, http/gateway/gateway-dispatch-spine]
|
|
scope: broad
|
|
risk: high
|
|
impact: component
|
|
level: 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")
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
#[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 `Arc`s (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<OperationRegistry> + Arc<dyn IdentityProvider>. 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. |