- HttpClientConfig.retry_policy: ExponentialBackoff (semver anchor to a reqwest-retry concrete type) replaced by retry: RetryConfig — an owned struct of plain scalars (max_retries, initial_backoff, max_retry_interval, defaults matching the previous backoff exactly); the ExponentialBackoff policy is built internally by the middleware stack; no reqwest_retry type is public anymore - ClientCertConfig fields documented (none had docs) - new tests/client_tls.rs: per-test rcgen private PKI + tokio-rustls HTTPS server; drives the real SharedHttpClient through HttpClientConfig file paths — CA-bundle success path, private-roots rejection (source-chain assertion: invalid peer certificate), mTLS end-to-end with client identity, mTLS rejection without identity, and reload-to-CA-bundle interplay - dev-deps: rcgen 0.14, tokio-rustls 0.26, rustls 0.23 (aws_lc_rs), rustls-pki-types 1, uuid Verified: cargo test (288 + 5 TLS), --all-features (359 + suites), --no-default-features (288; pre-existing warnings only), clippy --all-targets -D warnings (default + all-features), fmt --check, cargo doc --no-deps. Tasks: review-001-client-config-and-cert-coverage
115 lines
5.9 KiB
Markdown
115 lines
5.9 KiB
Markdown
---
|
||
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. |