Skip to content

Commit 304e4e7

Browse files
authored
fix(provider): retry anthropic overloaded errors (SSE event + HTTP 529) (#2595)
1 parent 03741f7 commit 304e4e7

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎crates/forge_domain/src/retry_config.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ impl Default for RetryConfig {
4242
min_delay_ms: 1000,
4343
backoff_factor: 2,
4444
max_retry_attempts: 8,
45-
retry_status_codes: vec![429, 500, 502, 503, 504, 408, 522, 520],
45+
retry_status_codes: vec![429, 500, 502, 503, 504, 408, 522, 520, 529],
4646
max_delay: None,
4747
suppress_retry_errors: false,
4848
}
@@ -71,7 +71,7 @@ mod tests {
7171
assert_eq!(config.max_retry_attempts, 8);
7272
assert_eq!(
7373
config.retry_status_codes,
74-
vec![429, 500, 502, 503, 504, 408, 522, 520]
74+
vec![429, 500, 502, 503, 504, 408, 522, 520, 529]
7575
);
7676
assert_eq!(config.suppress_retry_errors, false);
7777
}

‎crates/forge_repo/src/provider/openai_responses/repository.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,7 @@ mod tests {
10821082

10831083
assert_eq!(
10841084
repo.retry_config.retry_status_codes,
1085-
vec![429, 500, 502, 503, 504, 408, 522, 520]
1085+
vec![429, 500, 502, 503, 504, 408, 522, 520, 529]
10861086
);
10871087
}
10881088

‎crates/forge_repo/src/provider/retry.rs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use forge_app::domain::{Error as DomainError, RetryConfig};
2+
use forge_app::dto::anthropic::Error as AnthropicError;
23
use forge_app::dto::openai::{Error, ErrorResponse};
34

45
const TRANSPORT_ERROR_CODES: [&str; 3] = ["ERR_STREAM_PREMATURE_CLOSE", "ECONNRESET", "ETIMEDOUT"];
@@ -16,13 +17,22 @@ pub fn into_retry(error: anyhow::Error, retry_config: &RetryConfig) -> anyhow::E
1617
|| is_req_transport_error(&error)
1718
|| is_event_transport_error(&error)
1819
|| is_empty_error(&error)
20+
|| is_anthropic_overloaded_error(&error)
1921
{
2022
return DomainError::Retryable(error).into();
2123
}
2224

2325
error
2426
}
2527

28+
/// Checks if the error is an Anthropic `overloaded_error`, which arrives as an
29+
/// SSE event payload rather than an HTTP status code and must be retried.
30+
fn is_anthropic_overloaded_error(error: &anyhow::Error) -> bool {
31+
error
32+
.downcast_ref::<AnthropicError>()
33+
.is_some_and(|e| matches!(e, AnthropicError::OverloadedError { .. }))
34+
}
35+
2636
fn get_api_status_code(error: &anyhow::Error) -> Option<u16> {
2737
error.downcast_ref::<Error>().and_then(|error| match error {
2838
Error::Response(error) => error
@@ -294,6 +304,21 @@ mod tests {
294304
assert!(get_event_req_status_code(&error).is_none());
295305
}
296306

307+
#[test]
308+
fn test_anthropic_overloaded_error_is_retryable() {
309+
let retry_config = fixture_retry_config(vec![]);
310+
311+
// overloaded_error arriving as an SSE event must be retried
312+
let error = anyhow::Error::from(AnthropicError::OverloadedError {
313+
message: "Overloaded".to_string(),
314+
});
315+
assert!(is_retryable(into_retry(error, &retry_config)));
316+
317+
// Generic errors are still not retryable
318+
let error = anyhow!("Generic error");
319+
assert!(!is_retryable(into_retry(error, &retry_config)));
320+
}
321+
297322
#[tokio::test]
298323
async fn test_incomplete_message_is_retryable() {
299324
use tokio::net::TcpListener;

0 commit comments

Comments
 (0)