mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
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:
@@ -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<()> {
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user