diff --git a/rust/crates/api/src/providers/anthropic.rs b/rust/crates/api/src/providers/anthropic.rs index 83059a43..72001780 100644 --- a/rust/crates/api/src/providers/anthropic.rs +++ b/rust/crates/api/src/providers/anthropic.rs @@ -924,6 +924,22 @@ const fn is_retryable_status(status: reqwest::StatusCode) -> bool { matches!(status.as_u16(), 408 | 409 | 429 | 500 | 502 | 503 | 504) } +/// Some providers return HTTP 400 with an unparseable body when a gateway +/// or proxy flakes (e.g. "HTTP 400 from backend (no parseable body)"). +/// These are transient network blips, not actual bad requests, and should +/// be retried. We detect them by checking the body for known gateway error +/// phrases. +fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool { + if status != reqwest::StatusCode::BAD_REQUEST { + return false; + } + let lowered = body.to_ascii_lowercase(); + lowered.contains("no parseable body") + || lowered.contains("connection reset") + || lowered.contains("broken pipe") + || lowered.contains("empty reply from server") +} + /// Anthropic API keys (`sk-ant-*`) are accepted over the `x-api-key` header /// and rejected with HTTP 401 "Invalid bearer token" when sent as a Bearer /// token via `ANTHROPIC_AUTH_TOKEN`. This happens often enough in the wild diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index c349e5d1..00326f52 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -1682,6 +1682,21 @@ const fn is_retryable_status(status: reqwest::StatusCode) -> bool { matches!(status.as_u16(), 408 | 409 | 429 | 500 | 502 | 503 | 504) } +/// Some providers return HTTP 400 with an unparseable body when a gateway +/// or proxy flakes (e.g. "HTTP 400 from backend (no parseable body)"). +/// These are transient network blips, not actual bad requests, and should +/// be retried. +fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool { + if status != reqwest::StatusCode::BAD_REQUEST { + return false; + } + let lowered = body.to_ascii_lowercase(); + lowered.contains("no parseable body") + || lowered.contains("connection reset") + || lowered.contains("broken pipe") + || lowered.contains("empty reply from server") +} + /// Generate a suggested user action based on the HTTP status code and error context. /// This provides actionable guidance when API requests fail. fn suggested_action_for_status(status: reqwest::StatusCode) -> Option {