--- id: client-http-host name: Shared reqwest client host (middleware stack, retry-after) status: completed depends_on: [] scope: narrow risk: low impact: component level: implementation tags: [client, phase-3] --- ## Description Port the outbound HTTP client host from `/workspace/@alkdev/alknet/crates/alknet-http/src/client/`: `ClientWithMiddleware` shared across forwarding handlers; `RetryTransientMiddleware` (exponential backoff) + inlined `RetryAfterMiddleware` (parse Retry-After on 429/503, bounded URL→time map); ArcSwap rebuild-and-swap for config hot-reload. Credential injection is per-request from OperationContext.capabilities — never at client construction, never from env vars. ## Acceptance Criteria - [x] Client host ported with the middleware stack; inlined Retry-After (bounded storage) - [x] ArcSwap hot-swap tested (config change → new pool) - [x] No env-var reads (invariant test) - [x] `cargo test` passes ## References - docs/architecture/http-adapters.md (§HTTP client (reqwest)) - Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/client/{http_client,retry_after}.rs` ## Notes > Agent fills during implementation. ## Summary Ported `src/client/` (http_client.rs + retry_after.rs): SharedHttpClient (ArcSwap + ArcSwap, rebuild- and-swap reload), HttpClientConfig/ClientCertConfig, thiserror HttpClientBuildError (CaBundleRead/Parse, ClientCertRead/Parse, Build), concat_pem cert+key joining, RetryAfterMiddleware (429/503 Retry-After parse: seconds + HTTP-date via httpdate; bounded URL->deadline storage with earliest-deadline eviction; sleep-before-next-request). Convention applied: poisoned Mutex recovered via `unwrap_or_else(|e| e.into_inner())` per AGENTS.md error handling. 24 tests (12 retry-after incl. LRU eviction + sleep, 12 http client incl. ArcSwap reload ptr-neq + no-env-var invariant). 95 lib tests green.