From 414a1aca4fd35b6e3a5c7df787205aedab71afbf Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Mon, 27 Apr 2026 11:49:42 -0500 Subject: [PATCH] fix: retry 400 responses with transient gateway error bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some providers/proxies return HTTP 400 with bodies like "no parseable body" or "connection reset" during transient network blips. These are not real bad requests — they're gateway errors wearing a 400 mask. Detect known gateway error phrases in 400 response bodies and mark them as retryable so the existing exponential backoff handles them. --- rust/crates/api/src/providers/anthropic.rs | 16 ++++++++++++++++ rust/crates/api/src/providers/openai_compat.rs | 15 +++++++++++++++ 2 files changed, 31 insertions(+) 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 {