a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
57 lines
2.0 KiB
Markdown
57 lines
2.0 KiB
Markdown
# TLS `BadRecordMac` transport-retry reproduction
|
|
|
|
Standalone reproduction for the bug fixed in commit
|
|
`fix(providers): retry on TLS transport faults (BadRecordMac) across all providers`.
|
|
|
|
## What it reproduces
|
|
|
|
Users on flaky networks / corrupting middleboxes / some VPNs hit:
|
|
|
|
```
|
|
Stream error: IO error: received fatal alert: BadRecordMac
|
|
```
|
|
|
|
A `BadRecordMac` is a TLS record-authentication failure. It is transient: a
|
|
fresh connection on retry almost always succeeds. The bug was that jcode failed
|
|
these immediately instead of retrying, for two reasons:
|
|
|
|
1. **OpenAI** maintained its own `is_retryable_error` allowlist that did not
|
|
call the shared `is_transient_transport_error` and omitted every TLS term, so
|
|
a `BadRecordMac` on the websocket path surfaced at `attempt=1`
|
|
(`will_retry=false`).
|
|
2. **claude / copilot / openrouter / openai** classified on `e.to_string()`.
|
|
When a transport cause is wrapped behind `anyhow`'s `.context(...)`,
|
|
`to_string()` only returns the top-level context (e.g. `Failed to send
|
|
request to ... API`) and **masks** the underlying `BadRecordMac`, so the
|
|
classifier never sees it. (The anthropic provider already used
|
|
`format!("{e:#}")` and was unaffected.)
|
|
|
|
## How it works
|
|
|
|
```
|
|
reqwest client (rustls) -> corrupting TCP proxy -> real TLS server (rustls)
|
|
```
|
|
|
|
The proxy flips one byte inside the first large client->server TLS
|
|
`application_data` record. The server's AEAD check then fails and it emits a TLS
|
|
`bad_record_mac` fatal alert, which the client surfaces exactly as users see it.
|
|
|
|
The harness then:
|
|
|
|
- wraps the error like the providers do (`.context(...)`),
|
|
- runs the **verbatim** shipping `is_transient_transport_error`,
|
|
- runs the **old** vs **new** OpenAI `is_retryable_error`,
|
|
|
|
and asserts old logic misses it while the fix retries.
|
|
|
|
## Run
|
|
|
|
```bash
|
|
cd scripts/repro/tls-bad-record-mac
|
|
cargo run
|
|
# exit 0 + "SUCCESS: realistic BadRecordMac reproduced and both fixes validated."
|
|
```
|
|
|
|
It is intentionally a standalone crate (own `[workspace]` table) so its
|
|
TLS/cert dev-dependencies do not touch the main build.
|