Skip to content

feat(error): retain allowlisted response headers on status errors across SDKs - #419

Merged
benbenbang merged 4 commits into
mainfrom
feat/416-error-headers
Oct 6, 2026
Merged

benbenbang merged 4 commits into
mainfrom
feat/416-error-headers

Conversation

@benbenbang

Copy link
Copy Markdown
Member

Callers hitting rate-limiting or proxy-mediated failures previously had to parse error strings to learn when to retry or why a request was rejected. This change exposes that information as structured data while ensuring no sensitive headers leak.

Across the Go SDK, the Rust libfmp crate, and the Python bindings, status errors now retain a small allowlist of response headers (Retry-After, X-Proxy-*, X-RateLimit-*), compared case-insensitively, lowercased, redacted, bounded in size and count, and kept out of any error display text. Headers such as Set-Cookie, Authorization and ApiKey are never retained.

What changed:

  • Added header retention logic with a shared allowlist and redaction, filtering and bounding the retained values before attaching them to status and provider-message errors.
  • Exposed accessors for the retained data: retry-after parsing (delta-seconds and HTTP-date forms, with past dates treated as zero), the proxy error reason from X-Proxy-Error, and the raw allowlisted headers, surfaced idiomatically in each language (Go RetryAfter/ProxyError/Headers, Rust retry_after/proxy_error/headers, Python retry_after/proxy_error/headers).
  • Wired the retention into each client and transport so every status failure carries the allowlisted headers.
  • Added tests covering allowlist filtering, redaction, bounding and accessor behaviour, and extended the Python import and typecheck contract tests.
  • Documented the retained headers and new accessors in the Go, libfmp and Python READMEs.

Closes #416

This commit keeps the allowlisted `Retry-After`, `X-Proxy-*` and
`X-RateLimit-*` response headers on `Status` errors so callers can read
retry and proxy information without exposing anything sensitive.

**Error metadata:**
* Added a `Headers` field to `Error` that holds the allowlisted response
  headers, redacted and bounded; it is nil when none arrived and never
  appears in the `Error` text.
* `RetryAfter` returns the retry delay (delta-seconds or HTTP-date, with a
  past date treated as zero) and `ProxyError` returns the proxy's
  `X-Proxy-Error` reason.

**Transport wiring:**
* Introduced `headers.go` with the retention allowlist and the logic that
  filters and redacts the retained headers.
* `statusError` and the new `providerMessageError` method now populate the
  `Headers` field from the buffered response.
* Added `headers_test.go` to cover the allowlist and redaction behaviour,
  and documented the retained headers in the README.
This commit adds `SafeHeaders`, a redacted and bounded view of the
response headers kept for callers on status errors, and wires it into the
client so every status failure carries the allowlisted headers.

**Header retention contract:**
* Introduced `SafeHeaders`, which keeps only names accepted by the shared `is_retained_header_name` allowlist (`retry-after`, `x-proxy-*`, `x-ratelimit-*`, compared case-insensitively), lowercased and in response order.
* Values are passed through the `Redactor`, dropped when they are not visible ASCII or exceed `MAX_SAFE_HEADER_VALUE_BYTES`, and capped at `MAX_SAFE_HEADERS` entries; `set-cookie`, `authorization` and `apikey` are never retained.
* Added `get`, `iter`, `len` and `is_empty` accessors for inspecting the retained pairs.

**Retry and proxy accessors:**
* Added `retry_after` and `retry_after_at`, which parse delta-seconds and the IMF-fixdate, RFC 850 and asctime HTTP-date forms, yielding `Duration::ZERO` for a past date and `None` when absent or unparsable.
* Added `Error::headers`, `Error::retry_after` and `Error::proxy_error` (from `X-Proxy-Error`), keeping headers out of the `Display` output.

**Client wiring and tests:**
* Attached `SafeHeaders` to every status error raised by the client via `with_headers` and a `safe_headers` helper over the transport response.
* Added `tests/safe_headers.rs` covering the retention and accessor behaviour.
This commit adds client-level tests verifying that status and
provider-message errors retain only an allowlisted set of response
headers, and documents the behaviour in the libfmp README.

**Testing:**
* Added `crates/libfmp/tests/error_headers.rs` with client-level tests
  confirming that status errors and provider-message errors keep only
  the allowlisted headers (`Retry-After`, `X-Proxy-*`, `X-RateLimit-*`)
  and store no other header.

**Documentation:**
* Updated `crates/libfmp/README.md` to describe the allowlisted headers
  retained by a status error, the `err.retry_after()`, `err.proxy_error()`
  and `err.headers()` accessors, and the fact that headers never appear in
  the error's `Display` text.
This commit adds three structured attributes to the Python error hierarchy
so callers can react to rate-limiting proxies without parsing `str(error)`.

**New error attributes:**
* `FmpError` now carries `retry_after`, the `Retry-After` delay in seconds as
  a float (delta-seconds or HTTP-date, with a past date reported as `0.0`).
* `headers` is a dict of the lowercase allowlisted response headers
  (`retry-after`, `x-proxy-*`, `x-ratelimit-*`), redacted and `None` when none
  were sent; no other header is ever stored or shown in `str(error)`.
* `proxy_error` surfaces the proxy's `X-Proxy-Error` reason, such as
  `"rate_limited"`.

**Implementation and stubs:**
* `errors.rs` populates the new attributes from the native error, retaining
  only the allowlisted headers and setting defaults to `None` on the base.
* The `.pyi` stub and member metadata declare the three properties with docs.

**Tests and docs:**
* `conftest.py` routes can now send extra headers for exercising these cases.
* Added `test_error_headers.py` and extended the import and typecheck contract
  tests to cover the new attributes.
* Documented the attributes in the README error table.

close #416
@benbenbang
benbenbang enabled auto-merge (rebase) October 6, 2026 10:13
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request labels Oct 6, 2026
@benbenbang
benbenbang merged commit 9ebf240 into main Oct 6, 2026
10 checks passed
@benbenbang
benbenbang deleted the feat/416-error-headers branch October 6, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep Retry-After and X-Proxy-* headers on status errors

1 participant