Files
alkhttp/tasks/client/review-001-client-config-and-cert-coverage.md
T
glm-5.3-flash edbda6605b refactor(client): owned RetryConfig + TLS/mTLS test coverage (HY-06, COV-02)
- 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
2026-08-30 07:24:34 +00:00

5.9 KiB
Raw Blame History

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
review-001-client-config-and-cert-coverage Client config API cleanup (HY-06) + mTLS/CA-bundle test coverage (COV-02) completed
narrow low component implementation
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

  • HttpClientConfig no longer exposes ExponentialBackoff; owned field set covers what the remediation made configurable; module docs updated
  • Existing config-construction call sites migrated (adapters' test fixtures included)
  • 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)
  • Feature matrix green (default, --all-features, --no-default-features)
  • 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: ExponentialBackoffHttpClientConfig.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.