Keep response streams alive through connection failures (#37485)

## What changed

- Classify HTTP connection failures separately from other network errors without exposing request URLs.
- For sampling requests, retry connection failures with exponential delays from 5 to 60 seconds and show a `Reconnecting... waiting for network` stream error.
- Preserve the normal stream retry budget while waiting for the provider to become reachable. Keep the existing bounded retry behavior for other retryable errors.

## Testing

- Verify connection errors are classified without leaking URL contents.
- Verify a turn recovers after its provider becomes reachable and still applies the configured retry limit to a subsequent incomplete stream.

GitOrigin-RevId: 646553290c865a1332abd30c4a64ed9266bbfc6f
This commit is contained in:
jif
2026-08-07 18:25:30 +00:00
committed by copyberry
parent 509565820f
commit 5a0d0929e2
10 changed files with 162 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
//! Errors returned by the shared Codex HTTP transport.
use crate::client::HttpError;
use http::HeaderMap;
use http::StatusCode;
use thiserror::Error;
@@ -17,6 +18,8 @@ pub enum TransportError {
RetryLimit,
#[error("timeout")]
Timeout,
#[error("connection failed: {0}")]
Connection(#[source] HttpError),
#[error("network error: {0}")]
Network(String),
#[error("request build error: {0}")]

View File

@@ -78,7 +78,9 @@ impl ReqwestTransport {
}
fn map_error(err: reqwest::Error) -> TransportError {
if err.is_timeout() {
if err.is_connect() {
TransportError::Connection(err.without_url())
} else if err.is_timeout() {
TransportError::Timeout
} else {
TransportError::Network(err.to_string())

View File

@@ -28,6 +28,28 @@ async fn disabled_request_logging_suppresses_transport_url_and_body() {
assert!(!logs.contains("body-secret"));
}
#[tokio::test]
async fn connection_failures_are_classified_without_exposing_request_urls() {
let unavailable_server =
std::net::TcpListener::bind(("127.0.0.1", 0)).expect("server port should bind");
let server_addr = unavailable_server
.local_addr()
.expect("server listener should have an address");
drop(unavailable_server);
let transport = ReqwestTransport::from_http_client(HttpClient::new(test_reqwest_client()));
let request = Request::new(
Method::POST,
format!("http://{server_addr}/responses?token=url-secret"),
);
let error = match transport.stream(request).await {
Err(TransportError::Connection(error)) => error,
Err(error) => panic!("expected a connection failure, got {error}"),
Ok(_) => panic!("an unavailable server should not return a response"),
};
assert!(!error.to_string().contains("url-secret"));
}
fn test_reqwest_client() -> reqwest::Client {
reqwest::Client::builder()
.no_proxy()