Files
alkhttp/docs/architecture/decisions/039-http-server-and-client-host-colocated.md
T
glm-5.3-flash a80f9948b8 feat(build): feature-sided builds — server/client sides independently selectable (ADR-039 Amendment 1)
Split the feature graph so consumers pulling only the import adapters
(from_openapi / from_jsonschema / from_mcp) no longer compile the axum
/ hyper server stack, and server-only deployments no longer compile
reqwest. One crate, one import path — sides cut by features, not by a
crate split.

Feature graph:
- server (default): axum host, gateway, WS upgrade, to_openapi, to_mcp
- client (default): client host, forward, from_jsonschema, from_openapi
- openapi: shared OpenAPISpec model (implied by both sides)
- mcp: from_mcp needs client, to_mcp needs server
- wss: tungstenite transport (from_wss); tungstenite half of the
  shared WS↔byte-stream adapter
- h2/http1: hyper protocol features; imply server

Wire-contract neutral: gateway endpoints, ALPNs, and all public API
shapes unchanged; defaults keep both sides on.

Supporting changes:
- forward.rs drops its axum::body::Bytes type leak (bytes crate types)
- bounded_join + error-echo caps move to input_validation (usable by
  both sides; openapi_spec no longer imports from forward)
- byte_adapter: axum flavor compiles under server, tungstenite under
  wss; the generic pumps stay shared (WS-11)
- input_validation / openapi_spec import-only internals gated to the
  side that consumes them
- http-body-util moves to dev-dependencies (was test-only)
- integration-test required-features updated for the new sides
- from_wss unit tests (axum producer harness) gated to server

Verified: cargo test (defaults, 453) and --all-features (575) pass;
lean side builds (client / server / client,mcp / client,wss /
server,wss / openapi-only) build clean with zero warnings;
clippy -D warnings clean across all feature combinations; fmt clean.
2026-08-31 17:19:05 +00:00

9.1 KiB

ADR-039: HTTP Server and Client Host Colocated in alkhttp

Ported from alknet ADR-039 (HTTP Server and Client Host Colocated in alknet-http); re-targeted to alkhttp.

Status

Proposed

Context

alkhttp has two roles: an HTTP server (the HttpAdapter ProtocolHandler for h2/http/1.1, built on axum/hyper) and an HTTP client host (the from_openapi/from_mcp forwarding handlers, built on reqwest). The question is whether these two directions live in one crate (alkhttp) or are split into two crates (an HTTP-server crate + an HTTP-client crate).

ADR-003 lists the HTTP crate as a single crate with dependency alkcall, axum and justifies the per-handler-crate decomposition with "each handler is self-contained — it receives a byte stream and manages its own protocol." That rationale covers the server side (the HttpAdapter is self-contained), but it does not address the within-crate dual-role question: should the inbound HTTP server and the outbound HTTP client (the adapter forwarding handlers) be colocated, or split?

This is a load-bearing choice. Once published, downstream consumers build import paths against the crate boundary; the shared reqwest::Client and the no-env-vars invariant boundary (ADR-014) are scoped by it; the to_openapi/to_mcp projections are pure-registry-consumers that describe the server surface but live where the adapter types do. Splitting later would be a rewrite of every consumer's import paths, not a cheap revert. It needs an ADR.

Decision

One crate — alkhttp houses both the HTTP server and the HTTP client host (the adapter forwarding handlers and the to_* projections).

The two directions share the HTTP dependencies and HTTP-specific concerns that make splitting them counterproductive:

  • Shared HTTP dependencies. Both axum (server) and reqwest (client) pull in hyper, http, http-body, rustls/TLS stack types, and the HTTP header/status code types. A split into two crates would either duplicate these dependencies across both crates or force a third shared-types crate, neither of which is an improvement.
  • Shared HTTP-specific concerns. Both directions care about HTTP headers, status codes, content types, SSE framing, streaming vs non-streaming bodies, and TLS trust stores. The from_openapi forwarding handler's error mapping (HTTP status → HTTP_<status> error codes, ADR-023) and the to_openapi projection's error mapping (ErrorDefinition.http_status → HTTP response status) are the same mapping read in two directions — splitting them would put the two halves in different crates.
  • The to_* projections describe the server surface. to_openapi generates an OpenAPI doc whose paths mirror the gateway HTTP routes the HttpAdapter serves (ADR-036's mapping, superseded by ADR-047 — see ADR-047). to_mcp exposes the same operations as MCP tools. These projections consume the OperationRegistry and produce specs; they live with the adapter types (in alkhttp, per the adapter location map — see the alkcall crate docs, client-and-adapters) because they share the operation-spec→HTTP mapping logic with the server's request dispatch.
  • The no-env-vars invariant boundary is crate-scoped. The from_openapi/from_mcp forwarding handlers are the credential injection point (ADR-014). The invariant — "no handler reads outbound credentials from any source other than OperationContext.capabilities" — is verified against the handler implementations in this crate. A split would put the invariant verification boundary across two crates.

What this does NOT change

  • ADR-003's rule "no handler crate depends on another handler crate" applies to peer handler crates (alkhttp does not depend on alknet-ssh). The alkhttp → alkcall edge is the protocol-foundation exception (ADR-003 Amendment 1). This ADR is about the internal structure of alkhttp, not its dependency edges.
  • The adapter location map (the OperationAdapter trait in alkcall; the HTTP-backed adapter implementations in alkhttp) is unchanged. This ADR records why the HTTP-backed adapters live in the same crate as the HTTP server, not whether they live in alkhttp vs alkcall.

Consequences

Positive:

  • One crate, one set of HTTP dependencies, one HTTP-specific concern surface. No duplicated hyper/http types across two crates, no shared-types crate needed.
  • The to_* projections live with the server whose surface they describe, and with the adapter types they consume. The operation-spec → HTTP mapping logic is in one place.
  • The no-env-vars invariant verification boundary is one crate. The from_openapi/from_mcp handlers and the credential injection logic they share are co-located.
  • A downstream consumer wires one crate (alkhttp) into the HandlerRegistry and gets the full HTTP surface — server + adapters + projections. No two-crate wiring.

Negative:

  • A deployment that only needs the HTTP server (no from_openapi/from_ mcp forwarding) still compiles the reqwest dependency. Mitigated: the mcp feature is already gated (ADR-037); the from_openapi forwarding is always available but the reqwest client is only constructed if a from_openapi/from_mcp adapter is registered at assembly time. The dependency is compiled, the client is lazy.
  • A deployment that only needs the HTTP client (e.g., an agent crate that only uses from_openapi forwarding, no inbound HTTP) still compiles axum/hyper. This is the rarer case — the agent crate (alknet-agent) consumes alkcall directly for tool dispatch and uses from_openapi via alkhttp's adapter, but doesn't serve inbound HTTP itself. In practice, the agent deployment wires alkhttp for the adapters and the CLI wires it for the server; the compile cost is paid once per workspace, not once per deployment.
  • The crate is larger than a single-direction crate would be. This is the cost of colocating shared concerns; the alternative (two crates
    • a shared types crate) is more crates, not less code.

Assumptions

  1. The shared-HTTP-dependencies argument holds. axum and reqwest both pull in hyper and the http crate's types; the shared types (headers, status codes, method, URI) are the same. If a future version of axum or reqwest diverges its HTTP types (e.g., axum moves to a different HTTP implementation), this argument weakens. As of axum 0.7+ and reqwest 0.12+, both are built on hyper 1.x and share http types.

  2. The to_* projections share enough mapping logic with the server to justify colocation. The operation-spec → HTTP path/method/ error-status mapping is the same in both directions. If the projections turn out to be pure registry-consumers with no HTTP-mapping logic (just spec serialization), the colocation argument is weaker — but the current design (ADR-036, ADR-023) has them sharing the mapping.

Amendment 1: feature-sided builds (server / client)

The colocation decision stands — one crate, one import path — but the "the compile cost is paid once per workspace" mitigation proved insufficient: a consumer that only imports (consumer side) was forced to compile the axum/hyper server stack, and vice versa. The feature graph now sides the crate without splitting it:

  • server (default) — the axum Router host, the gateway routes, the WS upgrade path, to_openapi, to_mcp (with mcp).
  • client (default) — the outbound client host, the from_* import adapters (from_wss additionally needs wss).
  • openapi — the shared OpenAPISpec document model (implied by both sides; from_openapi needs it, to_openapi needs it).
  • mcpfrom_mcp requires client, to_mcp requires server.
  • wss — the tungstenite WS transport (from_wss; the tungstenite flavor of the shared WS↔byte-stream adapter).
  • h2 / http1 — hyper protocol features; imply server.

Default features keep both sides on (zero behavior change); lean builds use default-features = false + the side they need. The wire contract (gateway endpoints, ALPNs) is untouched — this is a dependency-graph change, not a surface change.

References

  • ADR-003 — crate decomposition (this ADR addresses the within-alkhttp dual-role question, not the dependency edge; Amendment 1 covers the alkcall edge)
  • ADR-014 — the no-env-vars invariant whose verification boundary is crate-scoped
  • ADR-017 — the adapter contract; to_* are projections
  • ADR-023 — the error mapping shared between from_openapi (status → code) and to_openapi (code → status)
  • ADR-036 — the HTTP path = operation path mapping shared between server dispatch and to_openapi
  • ADR-037 — the mcp feature gate
  • overview.md — the crate overview (the inline rationale for this decision is replaced by a pointer to this ADR)