diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 10e936f1..64ba8df8 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -238,18 +238,44 @@ pub struct UnexpectedResponseError { pub request_id: Option, } +const CLOUDFLARE_BLOCKED_MESSAGE: &str = + "Access blocked by Cloudflare. This usually happens when connecting from a restricted region"; + +impl UnexpectedResponseError { + fn friendly_message(&self) -> Option { + if self.status != StatusCode::FORBIDDEN { + return None; + } + + if !self.body.contains("Cloudflare") || !self.body.contains("blocked") { + return None; + } + + let mut message = format!("{CLOUDFLARE_BLOCKED_MESSAGE} (status {})", self.status); + if let Some(id) = &self.request_id { + message.push_str(&format!(", request id: {id}")); + } + + Some(message) + } +} + impl std::fmt::Display for UnexpectedResponseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "unexpected status {}: {}{}", - self.status, - self.body, - self.request_id - .as_ref() - .map(|id| format!(", request id: {id}")) - .unwrap_or_default() - ) + if let Some(friendly) = self.friendly_message() { + write!(f, "{friendly}") + } else { + write!( + f, + "unexpected status {}: {}{}", + self.status, + self.body, + self.request_id + .as_ref() + .map(|id| format!(", request id: {id}")) + .unwrap_or_default() + ) + } } } @@ -665,6 +691,35 @@ mod tests { }); } + #[test] + fn unexpected_status_cloudflare_html_is_simplified() { + let err = UnexpectedResponseError { + status: StatusCode::FORBIDDEN, + body: "Cloudflare error: Sorry, you have been blocked" + .to_string(), + request_id: Some("ray-id".to_string()), + }; + let status = StatusCode::FORBIDDEN.to_string(); + assert_eq!( + err.to_string(), + format!("{CLOUDFLARE_BLOCKED_MESSAGE} (status {status}), request id: ray-id") + ); + } + + #[test] + fn unexpected_status_non_html_is_unchanged() { + let err = UnexpectedResponseError { + status: StatusCode::FORBIDDEN, + body: "plain text error".to_string(), + request_id: None, + }; + let status = StatusCode::FORBIDDEN.to_string(); + assert_eq!( + err.to_string(), + format!("unexpected status {status}: plain text error") + ); + } + #[test] fn usage_limit_reached_includes_hours_and_minutes() { let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();