Decouple exec-server HTTP from reqwest types (#35059)

## What changed

- Rename `ReqwestHttpClient` to `RouteAwareHttpClient` to reflect that delegated HTTP uses Codex's shared route-aware transport.
- Use `codex_http_client` response and error types plus transport-neutral `http` and `url` types, removing the exec server's direct `reqwest` dependency.

## Testing

- Cover fragment stripping and Unicode hostname normalization for delegated HTTP requests.

GitOrigin-RevId: 8b0fc60a76004feb57198bfb4afb1371c9ceb1bd
This commit is contained in:
Celia Chen
2026-07-24 00:53:05 +00:00
committed by copyberry
parent 94ebae725e
commit 09241ae4db
18 changed files with 175 additions and 64 deletions
@@ -65,7 +65,7 @@ struct TlsInterceptingProxy {
}
/// Exercises the same `http/request` route used by remotely executed Streamable HTTP MCP calls.
/// Each RPC builds a fresh reqwest client. The first response sets `__cflb`, and the second response
/// Each RPC uses the shared route-aware client. The first response sets `__cflb`, and the second response
/// replaces it, proving cross-client persistence through the shared cookie store.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_server_replays_only_chatgpt_cloudflare_cookies() -> anyhow::Result<()> {
+6 -2
View File
@@ -20,8 +20,12 @@ async fn exec_server_serves_readyz_alongside_websocket_endpoint() -> anyhow::Res
.strip_prefix("ws://")
.expect("websocket URL should use ws://");
let response = reqwest::get(format!("http://{http_base_url}/readyz")).await?;
assert_eq!(response.status(), reqwest::StatusCode::OK);
let client = codex_http_client::HttpClientBuilder::new().build_direct()?;
let response = client
.get(format!("http://{http_base_url}/readyz"))
.send()
.await?;
assert_eq!(response.status(), http::StatusCode::OK);
server.shutdown().await?;
Ok(())
+105 -1
View File
@@ -113,6 +113,48 @@ async fn exec_server_http_request_buffers_response_body() -> anyhow::Result<()>
Ok(())
}
/// What this tests: delegated HTTP accepts URLs with client-side fragments and
/// does not include those fragments in the request sent over the network.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_server_http_request_omits_url_fragment() -> anyhow::Result<()> {
let mut server = exec_server().await?;
initialize_exec_server(&mut server).await?;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let url = format!(
"http://{}/mcp?case=fragment#client-section",
listener.local_addr()?
);
let http_request_id = server
.send_request(
"http/request",
serde_json::to_value(HttpRequestParams {
method: "GET".to_string(),
url,
headers: Vec::new(),
body: None,
timeout_ms: Some(5_000),
redirect_policy: HttpRedirectPolicy::Follow,
request_id: "fragment-request".to_string(),
stream_response: false,
})?,
)
.await?;
let captured = accept_http_request(&listener).await?;
assert_eq!(captured.request_line, "GET /mcp?case=fragment HTTP/1.1");
respond_with_status_and_headers(captured.stream, "200 OK", &[], b"fragment-response").await?;
let response: HttpRequestResponse = wait_for_response(&mut server, http_request_id).await?;
assert_eq!(
(response.status, response.body.into_inner()),
(200, b"fragment-response".to_vec())
);
server.shutdown().await?;
Ok(())
}
/// What this tests: a configured system-proxy factory survives the complete
/// executor transport, processor, and handler chain for delegated HTTP.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -168,6 +210,68 @@ async fn exec_server_http_request_uses_configured_system_proxy() -> anyhow::Resu
Ok(())
}
/// What this tests: delegated HTTP preserves WHATWG IDNA normalization before
/// selecting a configured system-proxy route.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_server_http_request_normalizes_unicode_hostname() -> anyhow::Result<()> {
let proxy_listener = TcpListener::bind("127.0.0.1:0").await?;
let proxy_url = format!("http://{}", proxy_listener.local_addr()?);
let request_url = "http://münich.invalid/mcp?route=unicode";
let normalized_url = "http://xn--mnich-kva.invalid/mcp?route=unicode";
let mut server = exec_server_with_env([
(SYSTEM_PROXY_REQUEST_URL_ENV, normalized_url),
(SYSTEM_PROXY_URL_ENV, proxy_url.as_str()),
("HTTP_PROXY", ""),
("http_proxy", ""),
("HTTPS_PROXY", ""),
("https_proxy", ""),
("ALL_PROXY", ""),
("all_proxy", ""),
("NO_PROXY", ""),
("no_proxy", ""),
])
.await?;
initialize_exec_server(&mut server).await?;
let http_request_id = server
.send_request(
"http/request",
serde_json::to_value(HttpRequestParams {
method: "GET".to_string(),
url: request_url.to_string(),
headers: Vec::new(),
body: None,
timeout_ms: Some(5_000),
redirect_policy: HttpRedirectPolicy::Follow,
request_id: "unicode-hostname-request".to_string(),
stream_response: false,
})?,
)
.await?;
let captured = accept_http_request(&proxy_listener).await?;
assert_eq!(
(
captured.request_line.as_str(),
captured.headers.get("host").map(String::as_str),
),
(
"GET http://xn--mnich-kva.invalid/mcp?route=unicode HTTP/1.1",
Some("xn--mnich-kva.invalid"),
)
);
respond_with_status_and_headers(captured.stream, "200 OK", &[], b"unicode-response").await?;
let response: HttpRequestResponse = wait_for_response(&mut server, http_request_id).await?;
assert_eq!(
(response.status, response.body.into_inner()),
(200, b"unicode-response".to_vec())
);
server.shutdown().await?;
Ok(())
}
/// What this tests: OAuth callers can inspect redirect responses without the
/// executor following the Location header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -662,7 +766,7 @@ fn is_expected_peer_disconnect(err: &anyhow::Error) -> bool {
})
}
/// Writes a chunked HTTP response so reqwest must drive the streaming path.
/// Writes a chunked HTTP response so the shared client must drive the streaming path.
async fn respond_with_chunked_body(
mut stream: TcpStream,
headers: &[(&str, &str)],
@@ -5,7 +5,7 @@ use std::sync::Mutex;
use codex_exec_server::HttpClient;
use codex_exec_server::HttpRedirectPolicy;
use codex_exec_server::HttpRequestParams;
use codex_exec_server::ReqwestHttpClient;
use codex_exec_server::RouteAwareHttpClient;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use pretty_assertions::assert_eq;
@@ -34,7 +34,7 @@ async fn delegated_http_success_logs_do_not_expose_sensitive_request_or_response
let _guard = tracing::subscriber::set_default(subscriber);
tracing::debug!(target: "codex_exec_server", "log capture sentinel");
let client =
ReqwestHttpClient::new(HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
RouteAwareHttpClient::new(HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
for (redirect_policy, status, query_secret, cookie_secret, location_secret) in [
(
@@ -128,7 +128,7 @@ async fn delegated_http_failure_warning_redacts_request_url() -> anyhow::Result<
let unavailable_address = unavailable_server.local_addr()?;
drop(unavailable_server);
let client =
ReqwestHttpClient::new(HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
RouteAwareHttpClient::new(HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
let error = client
.http_request(HttpRequestParams {