Files
alknet/docs/architecture/questions/040-reqwest-client-config-and-connection-pooling.md
T
glm-5.2 1baa619ce9 docs(arch): decompose open-questions.md into per-OQ files under questions/
The monolithic open-questions.md (1310 lines, 47 OQs) was large enough to be
unmanageable, with high size variance (OQ-42 at 220 lines next to OQ-06 at 8).
Decomposed into one file per OQ under docs/architecture/questions/ (NNN-slug.md,
mirroring the ADR convention), with open-questions.md retained as the index:
theme-grouped tables plus a cross-theme Deferred/Blocked section that surfaces
the 6 deferred OQs with their Blocked-on conditions inline (the safe-exit
visibility surface). Per-OQ content moved verbatim; all 62 inbound links stay
valid (none used anchors). README's curated OQ summary dropped (now redundant
with the index tables).

Also seeds tasks/architecture/ with this task plus two follow-ups found during
the decompose: OQ-09/10 missing structured Blocked-on fields, and the
tasks/architecture/ blocker-task half of the Safe Exit protocol being
unenforced.
2026-07-06 16:07:59 +00:00

4.0 KiB

OQ-40: reqwest Client Config and Connection Pooling

  • Origin: http-adapters.md, http-mcp.md, the alknet-http Phase 0 findings DH-7

  • Status: resolved (2026-06-30)

  • Door type: Two-way

  • Priority: low

  • Resolution: alknet-http owns a shared HTTP client constructed once and reused across all from_openapi/from_mcp forwarding handlers. The client carries connection pooling, keep-alive, TLS, and a retry stack. The config shape is:

    Aspect Decision
    Shared client type reqwest_middleware::ClientWithMiddleware (not a bare reqwest::Client) — required because both retry and Retry-After are middleware on the stack
    Middleware stack RetryTransientMiddleware (from reqwest-retry — exponential backoff on transient failures: connection errors, 5xx) + inlined RetryAfterMiddleware (parses the Retry-After header on 429/503 and sleeps before the next request to that URL)
    Retry-After handler Inlined from melotic/reqwest-retry-after (MIT, ~50 lines of real logic). The crate is complementary to reqwest-retry, not a replacement — reqwest-retry's default strategy does not honor Retry-After, which is why the separate middleware exists. Inlining lets the unbounded HashMap<Url, SystemTime> storage in the upstream crate be bounded (the melotic version grows without limit over a long-running process).
    Pooling / keep-alive / TLS reqwest::ClientBuilder defaults; system trust store for outbound HTTPS (standard calls to OpenAI, Anthropic, etc.)
    Hot-reload Rebuild-and-swap the ClientWithMiddleware via ArcSwap (same pattern as ConfigIdentityProvider, ADR-035). A rebuild drops the connection pool / keep-alive state — acceptable, since a config change wanting a fresh pool is the case that triggers it. Retry policy is baked into the middleware at ClientBuilder::build() time; live policy mutation is not supported by reqwest-retry (no cheap per-policy update path exists).
    Credentials Per-request from OperationContext.capabilities — see the one-way constraints below

    The one-way constraints (settled before this OQ, restated unchanged): (1) alknet-http owns its HTTP client — no env-var-based client config, no shared global client; (2) credential injection happens per-request (from OperationContext.capabilities), not at client construction — the client is shared across all operations, the credentials are per-call; (3) TLS for outbound calls uses the system trust store by default (custom CA bundle + client certs are an optional config for self-hosted API gateways).

    Downstream layering boundary (so the agent crate doesn't accidentally re-invent a client). The agent crate's provider SSE normalization (replicating the solid part of aisdk's pattern — the Vercel-UI-message normalization that maps different providers' SSE to a common shape) sits on top of this ClientWithMiddleware: it consumes the reqwest::Response stream the forwarding handler produces and emits call.responded events. It does not replace the client or own transport/pooling/retry. alknet-http owns transport; the agent crate owns provider-specific SSE → Vercel-UI-message mapping. The aisdk core/client.rs reference for HTTP client construction is not carried forward — its env-var config and hand-rolled retry are the anti-patterns being discarded; the aisdk/@alkdev/operations/src/from_openapi.ts SSE normalization pattern is separate and stays referenced in the forwarding-handler section of http-adapters.md.

    No ADR — the decision is internal to alknet-http: the client type does not cross crate boundaries (alknet-call never sees reqwest), the library choice is reversible, and it does not touch the system's structure, constraints, or API surface across crates.

  • Cross-references: ADR-014, ADR-017, ADR-035, http-adapters.md, http-mcp.md