mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
## 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
170 lines
4.6 KiB
Rust
170 lines
4.6 KiB
Rust
use crate::client::HttpClient;
|
|
use crate::client::RequestBuilder;
|
|
use crate::error::TransportError;
|
|
use crate::request::Request;
|
|
use crate::request::RequestBody;
|
|
use crate::request::Response;
|
|
use bytes::Bytes;
|
|
use futures::StreamExt;
|
|
use futures::stream::BoxStream;
|
|
use http::HeaderMap;
|
|
use http::Method;
|
|
use http::StatusCode;
|
|
use tracing::Level;
|
|
use tracing::enabled;
|
|
use tracing::trace;
|
|
|
|
pub type ByteStream = BoxStream<'static, Result<Bytes, TransportError>>;
|
|
|
|
pub struct StreamResponse {
|
|
pub status: StatusCode,
|
|
pub headers: HeaderMap,
|
|
pub bytes: ByteStream,
|
|
}
|
|
|
|
pub trait HttpTransport: Send + Sync {
|
|
fn execute(
|
|
&self,
|
|
req: Request,
|
|
) -> impl std::future::Future<Output = Result<Response, TransportError>> + Send;
|
|
fn stream(
|
|
&self,
|
|
req: Request,
|
|
) -> impl std::future::Future<Output = Result<StreamResponse, TransportError>> + Send;
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ReqwestTransport {
|
|
client: HttpClient,
|
|
}
|
|
|
|
impl ReqwestTransport {
|
|
pub fn new(client: reqwest::Client) -> Self {
|
|
Self {
|
|
client: HttpClient::new(client),
|
|
}
|
|
}
|
|
|
|
pub fn from_http_client(client: HttpClient) -> Self {
|
|
Self { client }
|
|
}
|
|
|
|
fn build(&self, req: Request) -> Result<RequestBuilder, TransportError> {
|
|
let prepared = req.prepare_body_for_send().map_err(TransportError::Build)?;
|
|
|
|
let Request {
|
|
method,
|
|
url,
|
|
headers: _,
|
|
body: _,
|
|
compression: _,
|
|
timeout,
|
|
} = req;
|
|
|
|
let mut builder = self.client.request(
|
|
Method::from_bytes(method.as_str().as_bytes()).unwrap_or(Method::GET),
|
|
&url,
|
|
);
|
|
|
|
if let Some(timeout) = timeout {
|
|
builder = builder.timeout(timeout);
|
|
}
|
|
|
|
builder = builder.headers(prepared.headers);
|
|
if let Some(body) = prepared.body {
|
|
builder = builder.body(body);
|
|
}
|
|
Ok(builder)
|
|
}
|
|
|
|
fn map_error(err: reqwest::Error) -> TransportError {
|
|
if err.is_connect() {
|
|
TransportError::Connection(err.without_url())
|
|
} else if err.is_timeout() {
|
|
TransportError::Timeout
|
|
} else {
|
|
TransportError::Network(err.to_string())
|
|
}
|
|
}
|
|
|
|
fn trace_request(&self, req: &Request) {
|
|
if self.client.request_logging_enabled() && enabled!(Level::TRACE) {
|
|
trace!(
|
|
"{} to {}: {}",
|
|
req.method,
|
|
req.url,
|
|
request_body_for_trace(req)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn request_body_for_trace(req: &Request) -> String {
|
|
match req.body.as_ref() {
|
|
Some(RequestBody::Json(body)) => body.to_string(),
|
|
Some(RequestBody::EncodedJson(body)) => {
|
|
String::from_utf8_lossy(body.trace_bytes()).into_owned()
|
|
}
|
|
Some(RequestBody::Raw(body)) => format!("<raw body: {} bytes>", body.len()),
|
|
None => String::new(),
|
|
}
|
|
}
|
|
|
|
impl HttpTransport for ReqwestTransport {
|
|
async fn execute(&self, req: Request) -> Result<Response, TransportError> {
|
|
self.trace_request(&req);
|
|
|
|
let url = req.url.clone();
|
|
let builder = self.build(req)?;
|
|
let resp = builder.send().await.map_err(Self::map_error)?;
|
|
let status = resp.status();
|
|
let headers = resp.headers().clone();
|
|
let bytes = resp.bytes().await.map_err(Self::map_error)?;
|
|
if !status.is_success() {
|
|
let body = String::from_utf8(bytes.to_vec()).ok();
|
|
return Err(TransportError::Http {
|
|
status,
|
|
url: Some(url),
|
|
headers: Some(headers),
|
|
body,
|
|
});
|
|
}
|
|
Ok(Response {
|
|
status,
|
|
headers,
|
|
body: bytes,
|
|
})
|
|
}
|
|
|
|
async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError> {
|
|
self.trace_request(&req);
|
|
|
|
let url = req.url.clone();
|
|
let builder = self.build(req)?;
|
|
let resp = builder.send().await.map_err(Self::map_error)?;
|
|
let status = resp.status();
|
|
let headers = resp.headers().clone();
|
|
if !status.is_success() {
|
|
let body = resp.text().await.ok();
|
|
return Err(TransportError::Http {
|
|
status,
|
|
url: Some(url),
|
|
headers: Some(headers),
|
|
body,
|
|
});
|
|
}
|
|
let stream = resp
|
|
.bytes_stream()
|
|
.map(|result| result.map_err(Self::map_error));
|
|
Ok(StreamResponse {
|
|
status,
|
|
headers,
|
|
bytes: Box::pin(stream),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "transport_tests.rs"]
|
|
mod tests;
|