Files
codex/codex-rs/exec-server/src/client_error.rs
T
Henry Levy 6e093c6081 Add timeout-aware exec-server configuration reads (#52679)
## Why

Remote configuration reads need a bounded wait without disrupting valid in-flight work on connections that cannot reconnect.

## What changed

- Add `read_environment_config_with_timeout` to `ExecServerClient` and `Environment`, bounding RPC sending and receiving after connection establishment. Local reads retain their existing behavior.
- Return a typed `ExecServerError::RpcTimedOut` and classify it as retryable for read-only preparation.
- Share timeout handling with environment-info requests. Close the timed-out transport and request recovery only when a reconnect strategy exists; otherwise, keep the connection open.

## Testing

Add a regression test verifying that a timed-out configuration read preserves an in-flight stdio read and allows subsequent reads on the same connection, including after a late response.

GitOrigin-RevId: 6785d5846f4c3b07a6e7c505e359238609c3a6b6
2026-10-09 19:22:21 +00:00

69 lines
2.8 KiB
Rust

//! 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::RpcTimedOut { .. }
| 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,
}
}
}