Files
1jehuang--jcode/crates/jcode-provider-core/src/transport.rs
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

114 lines
5.4 KiB
Rust

//! Shared transport-level error classification for provider runtimes.
//!
//! Every provider (Anthropic, OpenAI, Gemini, Cursor, ...) needs to decide
//! whether a request failure is a transient transport fault worth retrying on
//! a fresh connection, or a real error to surface. Keeping the classifier here
//! ensures all providers recognize the same fault vocabulary.
/// Whether an error message describes a transient transport-level fault
/// (connection reset, DNS hiccup, TLS teardown, HTTP/2 stream error, ...)
/// that is likely to succeed on retry with a fresh connection.
pub fn is_transient_transport_error(error_str: &str) -> bool {
let lower = error_str.to_ascii_lowercase();
lower.contains("connection reset")
|| lower.contains("connection closed")
|| lower.contains("connection refused")
|| lower.contains("connection aborted")
|| lower.contains("broken pipe")
|| lower.contains("timed out")
|| lower.contains("timeout")
|| lower.contains("operation timed out")
|| lower.contains("error decoding")
|| lower.contains("error reading")
|| lower.contains("unexpected eof")
// rustls reports an abrupt TCP close mid-stream as "peer closed
// connection without sending TLS close_notify" (its docs URL spells
// it "unexpected-eof", which the space-separated marker above does
// not match).
|| lower.contains("close_notify")
|| lower.contains("peer closed connection")
|| lower.contains("tls handshake eof")
// reqwest/hyper wrap connect-phase failures as "client error
// (Connect)" and connection-level faults as "connection error: ...".
|| lower.contains("client error (connect)")
|| lower.contains("connection error")
// hyper h1: connection closed while a response was still incomplete.
|| lower.contains("incomplete message")
|| lower.contains("request or response body error")
|| lower.contains("badrecordmac")
|| lower.contains("bad_record_mac")
|| lower.contains("fatal alert: badrecordmac")
|| lower.contains("fatal alert: bad_record_mac")
|| lower.contains("received fatal alert: badrecordmac")
|| lower.contains("received fatal alert: bad_record_mac")
|| lower.contains("decryption failed or bad record mac")
|| lower.contains("temporary failure in name resolution")
|| lower.contains("failed to lookup address information")
|| lower.contains("dns error")
|| lower.contains("name or service not known")
|| lower.contains("no route to host")
|| lower.contains("network is unreachable")
|| lower.contains("host is unreachable")
// HTTP/2 transport faults on reused/multiplexed connections. These are
// transient: a fresh connection on retry typically succeeds. Seen as
// "http2 error: stream error received: unspecific protocol error detected"
// or RST_STREAM / GOAWAY frames from the server or an intermediary.
|| lower.contains("http2 error")
|| lower.contains("stream error")
|| lower.contains("protocol error")
|| lower.contains("refused_stream")
|| lower.contains("refused stream")
|| lower.contains("enhance_your_calm")
|| lower.contains("goaway")
|| lower.contains("go away")
|| lower.contains("sendrequest")
}
#[cfg(test)]
mod tests {
use super::is_transient_transport_error;
#[test]
fn http2_stream_protocol_error_is_transient() {
// Exact shape reqwest/h2 surfaces for a reset on a reused HTTP/2 connection.
let msg = "error sending request for url (https://api.anthropic.com/v1/messages): \
client error (SendRequest): http2 error: stream error received: \
unspecific protocol error detected";
assert!(is_transient_transport_error(msg));
}
#[test]
fn http2_goaway_and_refused_stream_are_transient() {
assert!(is_transient_transport_error("http2 error: GOAWAY received"));
assert!(is_transient_transport_error("stream error: REFUSED_STREAM"));
}
#[test]
fn auth_errors_are_not_transient() {
assert!(!is_transient_transport_error("401 unauthorized"));
assert!(!is_transient_transport_error("invalid x-api-key"));
}
/// Real transport-error shapes harvested from ~/.jcode/logs.
#[test]
fn real_world_transport_errors_are_transient() {
let real_errors = [
"client error (Connect): dns error: failed to lookup address information: \
Name or service not known",
"client error (SendRequest): http2 error: keep-alive timed out: operation timed out",
"client error (SendRequest): connection error: peer closed connection without \
sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof",
"client error (Connect): operation timed out",
"client error (SendRequest): connection error: timed out",
"client error (Connect): tls handshake eof",
"error decoding response body: request or response body error: operation timed out",
];
for error in real_errors {
assert!(
is_transient_transport_error(error),
"should be transient: {error}"
);
}
}
}