mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Classify retryable exec-server preparation errors by type (#47638)
## Why Transient connection and registry failures can reach remote filesystem callers as generic `io::ErrorKind::Other` errors. Callers need a typed signal to identify failures that read-only preparation can retry. ## What changed - Add `ExecServerError::is_retryable_preparation_error()` to classify transient registry, connection, handshake, timeout, and session-attachment failures, including errors wrapped in `ConnectionAttempt`. - Map these failures to `io::ErrorKind::BrokenPipe` in the remote filesystem adapter. - Keep authentication, TLS, and malformed protocol failures outside the retryable classification; error text alone does not trigger it. ## Testing Add a regression test showing that a wrapped registry service-unavailable error maps to `BrokenPipe`, while a protocol error containing transport-closed text remains `Other`. GitOrigin-RevId: c3e1bbab8f977a0b421eba5d6d3071ada72c09ff
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
//! Classifies typed executor failures without treating error text as a retry signal.
|
||||
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use http::StatusCode;
|
||||
use tokio_tungstenite::tungstenite::Error as WebSocketError;
|
||||
use tokio_tungstenite::tungstenite::error::ProtocolError;
|
||||
|
||||
use crate::ExecServerError;
|
||||
use crate::client::is_retryable_registry_error;
|
||||
use crate::rpc::SESSION_ALREADY_ATTACHED_ERROR_CODE;
|
||||
|
||||
impl ExecServerError {
|
||||
/// Identifies transient failures that read-only preparation can retry.
|
||||
///
|
||||
/// Includes transient handshake responses and connection EOFs, including wrapped
|
||||
/// failures. Authentication, TLS, and malformed protocol errors are excluded.
|
||||
/// This is public because callers in `codex-core` also classify capability discovery.
|
||||
pub fn is_retryable_preparation_error(&self) -> bool {
|
||||
let mut error = self;
|
||||
while let Self::ConnectionAttempt(source) = error {
|
||||
error = source;
|
||||
}
|
||||
if is_retryable_registry_error(error) {
|
||||
return true;
|
||||
}
|
||||
match error {
|
||||
Self::Closed
|
||||
| Self::Disconnected(_)
|
||||
| Self::WebSocketConnectTimeout { .. }
|
||||
| Self::InitializeTimedOut { .. } => true,
|
||||
Self::WebSocketConnect { source, .. } => match source {
|
||||
WebSocketError::ConnectionClosed
|
||||
| WebSocketError::AlreadyClosed
|
||||
| WebSocketError::Protocol(
|
||||
ProtocolError::HandshakeIncomplete
|
||||
| ProtocolError::ResetWithoutClosingHandshake,
|
||||
) => true,
|
||||
WebSocketError::Http(response) => {
|
||||
response.status().is_server_error()
|
||||
|| matches!(
|
||||
response.status(),
|
||||
StatusCode::REQUEST_TIMEOUT
|
||||
| StatusCode::CONFLICT
|
||||
| StatusCode::TOO_MANY_REQUESTS
|
||||
)
|
||||
}
|
||||
WebSocketError::Io(error) => matches!(
|
||||
error.kind(),
|
||||
ErrorKind::ConnectionRefused
|
||||
| ErrorKind::ConnectionReset
|
||||
| ErrorKind::ConnectionAborted
|
||||
| ErrorKind::NetworkDown
|
||||
| ErrorKind::NetworkUnreachable
|
||||
| ErrorKind::HostUnreachable
|
||||
| ErrorKind::BrokenPipe
|
||||
| ErrorKind::NotConnected
|
||||
| ErrorKind::UnexpectedEof
|
||||
| ErrorKind::TimedOut
|
||||
),
|
||||
_ => false,
|
||||
},
|
||||
Self::Server { code, .. } if *code == SESSION_ALREADY_ATTACHED_ERROR_CODE => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user