--- id: review-001-client-config-and-cert-coverage name: Client config API cleanup (HY-06) + mTLS/CA-bundle test coverage (COV-02) status: completed depends_on: [] scope: narrow risk: low impact: component level: implementation tags: [client, review-001, follow-up, coverage] --- ## Description Two deferred client-host items, grouped (same file, `src/client/http_client.rs`): - **HY-06**: `HttpClientConfig.retry_policy` exposes `reqwest_retry::ExponentialBackoff` in the public API (`:116`) — a semver anchor to an upstream concrete type and an awkward construction surface. The post-remediation config (RetryGate, TotalRetryBudget, attempt counts, backoff bounds, budget deadline) is exactly why this should now be an owned config struct (e.g. `max_retries`, `max_retry_interval`, `initial_backoff`, `max_total_retry_duration`) from which the middleware stack builds the internal backoff. Breaks the public type — fine pre-publish; do it once, now, while the surface is small. - **COV-02 residue**: no test constructs a client with a working CA bundle or client cert — only the missing-file error paths are tested (`new_with_missing_ca_bundle_errors`). Post-remediation the redirect/retry gates hang off this client, so the TLS-config paths should be exercised: a test server with a self-signed cert + client presenting its own cert, happy path through the full middleware stack. ## Acceptance Criteria - [x] `HttpClientConfig` no longer exposes `ExponentialBackoff`; owned field set covers what the remediation made configurable; module docs updated - [x] Existing config-construction call sites migrated (adapters' test fixtures included) - [x] TLS test: client built with a CA bundle connects to a private-roots server; client-cert path exercised end-to-end (COV-02's uncovered build paths) - [x] Feature matrix green (default, `--all-features`, `--no-default-features`) - [x] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass ## References - docs/reviews/001-initial-implementation-review.md (Part H, HY-06; Part I, COV-02) - tasks/client/review-001-client-timeout-retry.md (the retry policy work this builds on) ## Notes **HY-06 shape chosen**: `HttpClientConfig.retry_policy: ExponentialBackoff` → `HttpClientConfig.retry: RetryConfig`, an owned struct of plain scalars `{ max_retries: u32, initial_backoff: Duration, max_retry_interval: Duration }` (debug-clone, `Default` = 3 retries / 100 ms / 2 s — the previous `ExponentialBackoff` default exactly). The jitter (`Bounded`) and exponential base (`2`) stay internal policy constants, not config — they were never surfaced before either. `RetryGateMiddleware::new` builds the internal `ExponentialBackoff` from the `RetryConfig` at client-construction time; `max_total_retry_duration` remains its own `HttpClientConfig` field (unchanged; it was already a plain scalar). `reqwest-retry` stays a private implementation detail of the middleware stack — no `reqwest_retry` type appears in the public API. Call-site audit: `retry_policy` had **zero** external construction sites — every `HttpClientConfig` consumer (from_jsonschema, from_openapi, openapi_spec, forward.rs, full_surface test) uses `HttpClientConfig::default()`; only `http_client.rs`'s own tests constructed the field. Those were migrated to `RetryConfig { .. }` literals, and the default-config assertions now pin `max_retries`, `initial_backoff`, and `max_retry_interval` on the owned struct. `ClientCertConfig` doc comments added (it had none — caught by the `missing_docs` gate re-measure this task unblocks). **COV-02 shape chosen**: new integration test file `tests/client_tls.rs` (kept out of the lib's test module — it needs `tokio-rustls`, `rustls`, `rcgen` as dev-deps only, so the base crate and its feature matrix stay lean). Per test it mints a throwaway private PKI with rcgen (CA + server leaf SAN `127.0.0.1`/`localhost` + client leaf with ClientAuth EKU), serves HTTPS/1.1 via tokio-rustls (`WebPkiClientVerifier` when mTLS is required), and drives the **real `SharedHttpClient`** through `HttpClientConfig` file paths — same encode path as production (PEM read → `add_root_certificate` / `Identity::from_pem` via `concat_pem`). Five tests: - CA-bundle client ↔ private-roots server: 200 + body intact + exactly 1 handshake (success path through the full middleware stack); - no-CA-bundle client rejected by private-roots server — asserts the TLS verification failure surfaces in the error **source chain** (`invalid peer certificate: UnknownIssuer`; the retry middleware wraps it, so the chain is walked, not the top-level Display); - mTLS: client presents identity → handshake completes end-to-end; - mTLS server rejects a client with no identity (TLS-level rejection); - `reload()` to a CA-bundle-backed client makes a previously unreachable server trusted (hot-reload × TLS interplay). The negative-path assertions mirror the retry-stack reality: the first attempt fails at the TLS layer and 3 retries surface a `Middleware(...)` wrapper — the source chain carries the answer. ## Summary - `src/client/http_client.rs`: `RetryConfig` (new public struct) replaces the `ExponentialBackoff` exposure; retry backoff built internally; `ClientCertConfig` fields documented; tests migrated (minimal_config, reload-swap, default assertions). - `tests/client_tls.rs` (new): private-PKI test server + 5 TLS tests (2 success-path TLS builds, 1 reload×TLS, 2 negative verification/mTLS rejections with chain-aware assertions). - `Cargo.toml`: dev-deps `rcgen 0.14`, `tokio-rustls 0.26`, `rustls 0.23` (no default features; aws_lc_rs + std + tls12), `rustls-pki-types 1`, `uuid` (v4 already a main dep). - Verified: `cargo test` (288 + 5 TLS), `cargo test --all-features` (359 + all suites), `cargo test --no-default-features` (288; same 4 pre-existing lib warnings as the base commit, nothing new), `cargo clippy --all-targets -- -D warnings` (default + all-features), `cargo fmt --check`, `cargo doc --no-deps` clean.