Harden error reporting, retry bounds, and credential handling

Second hardening pass, over the code added in 23c660d.

Error reporting:
- Decode failures now carry an excerpt of the body that arrived. The
  serde message alone ("expected value at line 1 column 1") gives no hint
  that the response was an HTML error page from a proxy or a login
  redirect from a wrong base URL. The excerpt is collapsed to one line
  and truncated so a large body cannot become a large error.
- API errors with an empty or message-less body report the status reason
  ("Not Found") rather than rendering blank.
- `Error` is `#[non_exhaustive]`, so future variants are not breaking.

Retry bounds:
- `RetryPolicy::max_elapsed` (120s default) bounds the time one call may
  spend across attempts. The request timeout is per attempt, so the
  previous defaults could spend four minutes on a call that kept timing
  out — a surprise for anyone reading `with_timeout(60)` as a bound on
  the call.
- `backoff` uses saturating arithmetic; a large `max_delay` could
  overflow the jitter span and panic in a debug build.
- Response bodies are drained before a retry so the connection returns to
  the pool instead of the next attempt opening a fresh one.

Credentials:
- Test pinning that neither the token nor the password appears in `Debug`
  output. `reqwest`'s `Client` prints its default headers, so the
  sensitive flag set when building them is the only thing keeping the
  token out of logs, and nothing enforced that.

Also drops `Option::is_none_or`, which raised the minimum toolchain to
1.82 for no benefit over a match.

91 tests, up from 80.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 18:33:43 -04:00
co-authored by Claude Opus 5
parent 23c660d2f9
commit 8ff947455e
8 changed files with 412 additions and 21 deletions
+8
View File
@@ -77,6 +77,14 @@ changes, doc tweaks) does not need an entry.
single place retrying happens. **A new helper must use both** — calling
`self.http` directly would skip the header, the timeout, and the retry
policy. Only `req` and `execute` should touch `self.http`.
- **The request timeout is per attempt, not per call.** `RetryPolicy::
max_elapsed` is what bounds a call overall; without it, four attempts at
the 60s default would take four minutes. Keep both in mind when changing
either.
- **Both credentials are stored as `HeaderValue`s with
`set_sensitive(true)`.** `reqwest`'s `Client` Debug prints its default
headers, so that flag is the only thing keeping the API token out of any
`{:?}` log. Don't build either header without it; a test pins this.
- `src/retry.rs` — `RetryPolicy` plus the rules for what may be replayed.
**The method matters, not just the status.** A 429 means the server
refused the request without processing it, so any method is safe to