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.
55 lines
4.0 KiB
Markdown
55 lines
4.0 KiB
Markdown
# OQ-40: reqwest Client Config and Connection Pooling
|
|
|
|
- **Origin**: [http-adapters.md](crates/http/http-adapters.md),
|
|
[http-mcp.md](crates/http/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](crates/http/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](crates/http/http-adapters.md),
|
|
[http-mcp.md](crates/http/http-mcp.md)
|