Skip to content

Commit bf29b6b

Browse files
authored
Merge pull request #88 from AI45Lab/fix/network-retry
v4.2.8 — retry transient network errors (not just HTTP status codes)
2 parents 743732a + 50de609 commit bf29b6b

15 files changed

Lines changed: 117 additions & 36 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "4.2.7"
3+
version = "4.2.8"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/llm/anthropic.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,23 @@ impl AnthropicClient {
356356
match result {
357357
Ok(r) => r,
358358
Err(e) => {
359-
return AttemptOutcome::Fatal(anyhow::anyhow!("HTTP request failed: {}", e));
359+
// A transient network error (timeout, reset,
360+
// mid-flight drop — common on throttled
361+
// endpoints) carries no HTTP status. Retry it
362+
// with backoff like 429/5xx instead of failing
363+
// the turn; a real fatal error still bails.
364+
return if crate::retry::is_transient_error(&e) {
365+
AttemptOutcome::Retryable {
366+
status: reqwest::StatusCode::SERVICE_UNAVAILABLE,
367+
body: format!("network error: {e}"),
368+
retry_after: None,
369+
}
370+
} else {
371+
AttemptOutcome::Fatal(anyhow::anyhow!(
372+
"HTTP request failed: {}",
373+
e
374+
))
375+
};
360376
}
361377
}
362378
}

core/src/llm/openai.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,23 @@ impl OpenAiClient {
573573
match result {
574574
Ok(r) => r,
575575
Err(e) => {
576-
return AttemptOutcome::Fatal(anyhow::anyhow!("HTTP request failed: {}", e));
576+
// Transient network error (timeout, reset,
577+
// mid-flight drop — common on throttled
578+
// endpoints): retry with backoff like 429/5xx
579+
// instead of failing the turn. GLM and other
580+
// OpenAI-compatible endpoints hit this most.
581+
return if crate::retry::is_transient_error(&e) {
582+
AttemptOutcome::Retryable {
583+
status: reqwest::StatusCode::SERVICE_UNAVAILABLE,
584+
body: format!("network error: {e}"),
585+
retry_after: None,
586+
}
587+
} else {
588+
AttemptOutcome::Fatal(anyhow::anyhow!(
589+
"HTTP request failed: {}",
590+
e
591+
))
592+
};
577593
}
578594
}
579595
}

core/src/retry.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,59 @@ where
183183
)
184184
}
185185

186+
/// Heuristic: is this a transient *network* error worth retrying — a timeout,
187+
/// connection reset/refused/closed, broken pipe, DNS failure, or a request that
188+
/// dropped mid-flight? These carry no HTTP status (so `is_retryable_status`
189+
/// can't see them), yet Claude Code retries them just like 429/5xx. We only have
190+
/// the error's rendered text (a `CodeError`/`anyhow::Error` chain) to classify.
191+
pub fn is_transient_error<E: std::fmt::Display>(e: &E) -> bool {
192+
let m = e.to_string().to_lowercase();
193+
[
194+
"timed out",
195+
"timeout",
196+
"connection reset",
197+
"connection refused",
198+
"connection closed",
199+
"connection aborted",
200+
"connection error",
201+
"broken pipe",
202+
"reset by peer",
203+
"error sending request",
204+
"incomplete message",
205+
"unexpected eof",
206+
"dns error",
207+
"unreachable",
208+
"tls handshake",
209+
"request error",
210+
"body error",
211+
"decoding response",
212+
"channel closed",
213+
"stream closed",
214+
]
215+
.iter()
216+
.any(|p| m.contains(p))
217+
}
218+
186219
#[cfg(test)]
187220
mod tests {
188221
use super::*;
189222
use std::sync::atomic::{AtomicU32, Ordering};
190223
use std::sync::Arc;
191224

225+
#[test]
226+
fn transient_error_classification() {
227+
let t = |s: &str| is_transient_error(&anyhow::anyhow!("{s}"));
228+
// Transient network errors → retry.
229+
assert!(t("error sending request for url: operation timed out"));
230+
assert!(t("connection reset by peer"));
231+
assert!(t("LLM error: connection closed before message completed"));
232+
assert!(t("tls handshake eof"));
233+
// Real application errors → do NOT retry.
234+
assert!(!t("invalid api key"));
235+
assert!(!t("model not found"));
236+
assert!(!t("context length exceeded"));
237+
}
238+
192239
// ========================================================================
193240
// RetryConfig unit tests
194241
// ========================================================================

sdk/node/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "4.2.7"
3+
version = "4.2.8"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -11,7 +11,7 @@ description = "A3S Code Node.js bindings - Native addon via napi-rs"
1111
crate-type = ["cdylib"]
1212

1313
[dependencies]
14-
a3s-code-core = { version = "4.2.7", path = "../../core", features = ["ahp", "s3", "serve"] }
14+
a3s-code-core = { version = "4.2.8", path = "../../core", features = ["ahp", "s3", "serve"] }
1515
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1616
napi-derive = "2"
1717
tokio = { version = "1.35", features = ["full"] }

sdk/node/examples/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a3s-lab/code",
3-
"version": "4.2.7",
3+
"version": "4.2.8",
44
"description": "A3S Code - Native Node.js bindings for the coding-agent runtime",
55
"main": "index.js",
66
"types": "index.d.ts",
@@ -43,11 +43,11 @@
4343
"test:helpers": "node test-helpers.mjs"
4444
},
4545
"optionalDependencies": {
46-
"@a3s-lab/code-darwin-arm64": "4.2.7",
47-
"@a3s-lab/code-linux-x64-gnu": "4.2.7",
48-
"@a3s-lab/code-linux-x64-musl": "4.2.7",
49-
"@a3s-lab/code-linux-arm64-gnu": "4.2.7",
50-
"@a3s-lab/code-linux-arm64-musl": "4.2.7",
51-
"@a3s-lab/code-win32-x64-msvc": "4.2.7"
46+
"@a3s-lab/code-darwin-arm64": "4.2.8",
47+
"@a3s-lab/code-linux-x64-gnu": "4.2.8",
48+
"@a3s-lab/code-linux-x64-musl": "4.2.8",
49+
"@a3s-lab/code-linux-arm64-gnu": "4.2.8",
50+
"@a3s-lab/code-linux-arm64-musl": "4.2.8",
51+
"@a3s-lab/code-win32-x64-msvc": "4.2.8"
5252
}
5353
}

0 commit comments

Comments
 (0)