--- id: review-001-client-timeout-retry name: Client redirect/Retry-After policy — idempotency, timeouts, caps (FWD-03, FWD-04, FWD-05, FWD-11, FWD-09) status: pending depends_on: [] scope: moderate risk: medium impact: component level: implementation tags: [client, review-001] --- ## Description Review 001 findings on the outbound client host (`src/client/http_client.rs`, `retry_after.rs`), grouped because they all shape the shared client's request policy: - **FWD-03**: no explicit redirect policy → reqwest's default cross-host redirect scrub removes only `Authorization`/`Cookie`/etc; `HttpAuthScheme::ApiKey { header_name }` credentials and `default_headers` follow a 302 to an attacker host intact (verified against reqwest 0.13 source). Fix: explicit policy — none, or limited same-host. - **FWD-04**: `RetryTransientMiddleware` retries POSTs (5xx/408/429/ timeout classified retryable regardless of method — verified in reqwest-retry 0.9.1) → duplicate upstream side effects; backoff has no total-duration cap. Fix: skip retries for non-idempotent methods (or make idempotency a per-adapter policy) and cap total retry wall time. - **FWD-05**: `HttpClientConfig::default()` sets `request_timeout: None` and no connect timeout anywhere; `retry_after.rs:27-37` accepts any u64 `Retry-After` with no maximum — a hostile backend's 10-year deadline stalls calls to that URL indefinitely. Fix: default request + connect timeouts (the gateway's 30 s deadline is the natural anchor) and a `Retry-After` ceiling (e.g. 300 s, configurable). - **FWD-11**: `Retry-After` is keyed on the pre-redirect URL, eviction drops the *earliest* deadline (keeping year-long entries), and all waiters wake together with no jitter. Key on the effective URL, evict sensibly, add jitter. - **FWD-09**: blocking `std::fs::read` in `build_client`, reachable via the public documented hot-reload path `SharedHttpClient::reload` — `tokio::fs`/`spawn_blocking` the reads. ## Acceptance Criteria - [ ] Cross-host redirect test with an API-key credential header — key must not reach the redirect target - [ ] Non-idempotent method is never retried (test); total retry duration bounded (test) - [ ] Default request + connect timeouts exist in `HttpClientConfig::default()`; `Retry-After` capped (tests) - [ ] Retry-After keyed post-redirect; eviction and wake behavior fixed or documented (tests) - [ ] No blocking fs reads on the async path (FWD-09) - [ ] `HttpClientConfig` defaults documented; review-001-forward-url-safety and this task together close the deployment-facing gate - [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass ## References - docs/reviews/001-initial-implementation-review.md (Part D, FWD-03, FWD-04, FWD-05, FWD-09, FWD-11) - docs/architecture/decisions/039-http-server-and-client-host-colocated.md ## Notes > Agent fills during implementation. Independent of > review-001-forward-url-safety (different file); they form the > deployment gate together. ## Summary > Filled on completion.