//! Route-aware local HTTP capability implementation. //! //! This code runs wherever the real network request should originate: //! - in a local environment, that means the orchestrator process //! - in a remote environment, that means the remote runtime after the //! orchestrator has forwarded `http/request` over JSON-RPC use std::time::Duration; use codex_exec_server_protocol::JSONRPCErrorError; use codex_http_client::ClientRouteClass; use codex_http_client::HttpClientFactory; use codex_http_client::RouteAwareClientPool; use codex_http_client::RouteAwareRequestError; use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR; use codex_protocol::shell_environment::OPENAI_FEDERATION_RULE_ID_ENV_VAR; use codex_protocol::shell_environment::OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR; use codex_protocol::shell_environment::OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR; use futures::FutureExt; use futures::StreamExt; use futures::future::BoxFuture; use http::HeaderMap; use http::HeaderName; use http::HeaderValue; use http::Method; use tracing::Instrument; use url::Url; use super::HttpResponseBodyStream; use super::response_body_stream::send_body_delta; use crate::HttpClient; use crate::client::ExecServerError; use crate::protocol::HttpHeader; use crate::protocol::HttpRedirectPolicy; use crate::protocol::HttpRequestBodyDeltaNotification; use crate::protocol::HttpRequestParams; use crate::protocol::HttpRequestResponse; use crate::protocol::MAX_HTTP_BODY_DELTA_BYTES; use crate::rpc::RpcNotificationSender; use crate::rpc::internal_error; use crate::rpc::invalid_params; const HTTP_HEADER_ENV_DENYLIST: &[&str] = &[ CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR, OPENAI_FEDERATION_RULE_ID_ENV_VAR, OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR, OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR, "OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN", "CODEX_CONNECTORS_TOKEN", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AZURE_CLIENT_SECRET", "AZURE_FEDERATED_TOKEN_FILE", "GOOGLE_APPLICATION_CREDENTIALS", ]; /// HTTP capability implementation backed by the shared route-aware transport. #[derive(Clone)] pub struct RouteAwareHttpClient { follow_redirects: RouteAwareClientPool, stop_redirects: RouteAwareClientPool, } /// Streaming response state held between the initial HTTP response and /// downstream body-delta forwarding. pub(crate) struct PendingRouteAwareHttpBodyStream { pub(crate) request_id: String, pub(crate) response: codex_http_client::HttpResponse, } /// Validates `http/request` parameters and runs the actual HTTP call used /// by the exec-server route and the local [`HttpClient`] backend. pub(crate) struct RouteAwareHttpRequestRunner { client: RouteAwareClientPool, } impl RouteAwareHttpClient { pub fn new(http_client_factory: HttpClientFactory) -> Self { Self { follow_redirects: RouteAwareClientPool::with_chatgpt_cloudflare_cookies_without_request_logging( http_client_factory.clone(), // Delegated HTTP targets arbitrary endpoints; route class only labels diagnostics. ClientRouteClass::Other, ), stop_redirects: RouteAwareClientPool::with_chatgpt_cloudflare_cookies_without_redirects_or_request_logging( http_client_factory, // Proxy routing comes from the factory, not this diagnostic-only route class. ClientRouteClass::Other, ), } } /// Enables narrowly scoped TLS-backend fallback for both redirect policies. pub fn with_tls_backend_fallback(mut self) -> Self { self.follow_redirects = self.follow_redirects.with_tls_backend_fallback(); self.stop_redirects = self.stop_redirects.with_tls_backend_fallback(); self } pub(crate) fn runner( &self, redirect_policy: HttpRedirectPolicy, ) -> RouteAwareHttpRequestRunner { let client = match redirect_policy { HttpRedirectPolicy::Follow => self.follow_redirects.clone(), HttpRedirectPolicy::Stop => self.stop_redirects.clone(), }; RouteAwareHttpRequestRunner { client } } } impl HttpClient for RouteAwareHttpClient { fn http_request( &self, params: HttpRequestParams, ) -> BoxFuture<'_, Result> { async move { let runner = self.runner(params.redirect_policy); let (response, _) = runner .run(HttpRequestParams { stream_response: false, ..params }) .await .map_err(|error| ExecServerError::HttpRequest(error.message))?; Ok(response) } .boxed() } fn http_request_stream( &self, params: HttpRequestParams, ) -> BoxFuture<'_, Result<(HttpRequestResponse, HttpResponseBodyStream), ExecServerError>> { async move { let runner = self.runner(params.redirect_policy); let (response, pending_stream) = runner .run(HttpRequestParams { stream_response: true, ..params }) .await .map_err(|error| ExecServerError::HttpRequest(error.message))?; let pending_stream = pending_stream.ok_or_else(|| { ExecServerError::Protocol( "http request stream did not return a response body stream".to_string(), ) })?; Ok(( response, HttpResponseBodyStream::local(pending_stream.response), )) } .boxed() } } impl RouteAwareHttpRequestRunner { pub(crate) async fn run( &self, params: HttpRequestParams, ) -> Result<(HttpRequestResponse, Option), JSONRPCErrorError> { let method = Method::from_bytes(params.method.as_bytes()) .map_err(|error| invalid_params(format!("http/request method is invalid: {error}")))?; let url = Url::parse(¶ms.url) .map_err(|error| invalid_params(format!("http/request url is invalid: {error}")))?; match url.scheme() { "http" | "https" => {} scheme => { return Err(invalid_params(format!( "http/request only supports http and https URLs, got {scheme}" ))); } } let request_span = tracing::info_span!( "codex.exec_server.http_request", otel.kind = "client", http.request.method = method.as_str(), server.address = url.host_str().unwrap_or_default(), server.port = u64::from(url.port_or_known_default().unwrap_or_default()), http.response.status_code = tracing::field::Empty, error.type = tracing::field::Empty, ); let mut headers = Self::build_headers(params.headers)?; codex_otel::inject_span_w3c_trace_headers(&request_span, &mut headers); let mut request = self.client.request(method.clone(), url).headers(headers); if let Some(body) = params.body { request = request.body(body.into_inner()); } if let Some(timeout_ms) = params.timeout_ms { request = request.timeout(Duration::from_millis(timeout_ms)); } let response = match request.send().instrument(request_span.clone()).await { Ok(response) => response, Err(error) => { request_span.record("error.type", "request"); let error_message = error.to_string(); log_send_error(&method, error); return Err(internal_error(format!( "http/request failed: {error_message}" ))); } }; let status = response.status().as_u16(); request_span.record("http.response.status_code", u64::from(status)); let headers = Self::response_headers(response.headers()); if params.stream_response { return Ok(( HttpRequestResponse { status, headers, body: Vec::new().into(), }, Some(PendingRouteAwareHttpBodyStream { request_id: params.request_id, response, }), )); } let body = response.bytes().await.map_err(|error| { internal_error(format!( "failed to read http/request response body: {error}" )) })?; Ok(( HttpRequestResponse { status, headers, body: body.to_vec().into(), }, None, )) } pub(crate) async fn stream_body( pending_stream: PendingRouteAwareHttpBodyStream, notifications: RpcNotificationSender, ) { let PendingRouteAwareHttpBodyStream { request_id, response, } = pending_stream; let mut seq = 1; let mut body = response.bytes_stream(); while let Some(chunk) = body.next().await { match chunk { Ok(bytes) => { for chunk in bytes.chunks(MAX_HTTP_BODY_DELTA_BYTES) { if !send_body_delta( ¬ifications, HttpRequestBodyDeltaNotification { request_id: request_id.clone(), seq, delta: chunk.to_vec().into(), done: false, error: None, }, ) .await { return; } seq += 1; } } Err(error) => { let _ = send_body_delta( ¬ifications, HttpRequestBodyDeltaNotification { request_id, seq, delta: Vec::new().into(), done: true, error: Some(error.to_string()), }, ) .await; return; } } } let _ = send_body_delta( ¬ifications, HttpRequestBodyDeltaNotification { request_id, seq, delta: Vec::new().into(), done: true, error: None, }, ) .await; } fn build_headers(headers: Vec) -> Result { let mut header_map = HeaderMap::new(); for header in headers { let name = HeaderName::from_bytes(header.name.as_bytes()).map_err(|error| { invalid_params(format!("http/request header name is invalid: {error}")) })?; let value = match header.value_env_var { Some(env_var) => { if HTTP_HEADER_ENV_DENYLIST .iter() .any(|denied| denied.eq_ignore_ascii_case(&env_var)) { return Err(invalid_params(format!( "http/request header {} cannot use executor environment variable {env_var}", header.name ))); } let env_value = std::env::var(&env_var).map_err(|_| { invalid_params(format!( "http/request header {} requires executor environment variable {env_var}", header.name )) })?; if env_value.is_empty() { return Err(invalid_params(format!( "http/request header {} requires a non-empty executor environment variable {env_var}", header.name ))); } format!("{}{env_value}", header.value) } None => header.value, }; let value = HeaderValue::from_str(&value).map_err(|error| { invalid_params(format!( "http/request header value is invalid for {}: {error}", header.name )) })?; header_map.append(name, value); } Ok(header_map) } fn response_headers(headers: &HeaderMap) -> Vec { headers .iter() .filter_map(|(name, value)| { Some(HttpHeader { name: name.as_str().to_string(), value: value.to_str().ok()?.to_string(), value_env_var: None, }) }) .collect() } } fn log_send_error(method: &Method, error: RouteAwareRequestError) { let error_is_timeout = error.is_timeout(); let error_is_connect = error.is_connect(); let error = match error { RouteAwareRequestError::Request(error) => error.without_url().to_string(), error => error.to_string(), }; tracing::warn!( http_method = method.as_str(), error_is_timeout, error_is_connect, error = %error, "http/request send failed" ); }