Files
codex/codex-rs/http-client/src/bin/custom_ca_probe.rs
T
Michael Bolin 9acfe8965d Extract shared HTTP transport into codex-http-client (#31323)
## Why

Codex-owned HTTP construction currently lives in `codex-client`
alongside higher-level retry, SSE, and request-telemetry policy. That
makes it difficult to apply shared network behavior consistently across
crates, particularly system proxy/PAC resolution, custom CA handling,
and the ChatGPT Cloudflare cookie policy. It also leaves no clear crate
boundary for migrating direct `reqwest` usage behind a single Codex
abstraction.

This change establishes that low-level ownership boundary without
changing request behavior. It builds on the system proxy support
introduced in #26706, #26707, #26708, and #26709.

## What changed

- Added `codex-rs/http-client` as the `codex-http-client` crate.
- Moved request/response types, the concrete `reqwest` transport, custom
CA handling, Cloudflare cookie policy, and macOS/Windows proxy
resolution into the new crate.
- Kept retry, SSE, and request-telemetry policy in `codex-client`.
- Re-exported the moved API from `codex-client`, including compatibility
aliases for `CodexHttpClient` and `CodexRequestBuilder`, so existing
consumers do not change in this PR.
- Moved the existing proxy and custom-CA tests with their
implementation.

## Scope boundary

This PR deliberately stops at the crate extraction. Stacked follow-up
#31331 migrates downstream imports from `codex-client` to
`codex-http-client`, keeping this change focused on ownership and
compatibility rather than mixing in repository-wide call-site churn.

## Review guide

GitHub reports 30 changed files, of which 17 are detected renames. A
useful review order is:

1. Review the new boundary in `codex-rs/http-client/Cargo.toml` and
`codex-rs/http-client/src/lib.rs`.
2. Review `codex-rs/codex-client/Cargo.toml` and
`codex-rs/codex-client/src/lib.rs` for what remains in the higher-level
crate and how compatibility is preserved.
3. Treat the renamed implementation and test files as moves. Their
meaningful edits are limited to crate paths and normalizing the new
crate's type names to `HttpClient` and `RequestBuilder`.
4. Review `codex-rs/Cargo.toml`, `codex-rs/Cargo.lock`, and the two
`BUILD.bazel` files as mechanical workspace integration.

## Test plan

- `just test -p codex-http-client -p codex-client` (38 tests)
- Compile-checked the unchanged `codex-api`, `codex-backend-client`,
`codex-cloud-tasks`, `codex-exec-server`, `codex-login`, and
`codex-model-provider` consumers against the compatibility re-exports.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31323).
* #31331
* __->__ #31323
2026-07-07 01:12:24 +00:00

101 lines
3.5 KiB
Rust

//! Helper binary for exercising shared custom CA environment handling in tests.
//!
//! The shared reqwest client honors `CODEX_CA_CERTIFICATE` and `SSL_CERT_FILE`, but those
//! environment variables are process-global and unsafe to mutate in parallel test execution. This
//! probe keeps the behavior under test while letting integration tests (`tests/ca_env.rs`) set
//! env vars per-process, proving:
//!
//! - env precedence is respected,
//! - multi-cert PEM bundles load,
//! - error messages guide users when CA files are invalid.
//! - optional HTTPS probes can complete a request through the constructed client.
//!
//! The detailed explanation of what "hermetic" means here lives in `codex_http_client::custom_ca`.
//! This binary exists so the tests can exercise
//! [`codex_http_client::build_reqwest_client_for_subprocess_tests`] in a separate process without
//! duplicating client-construction logic.
use std::env;
use std::process;
use std::time::Duration;
const PROBE_TLS13_ENV: &str = "CODEX_CUSTOM_CA_PROBE_TLS13";
const PROBE_PROXY_ENV: &str = "CODEX_CUSTOM_CA_PROBE_PROXY";
const PROBE_URL_ENV: &str = "CODEX_CUSTOM_CA_PROBE_URL";
fn main() {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
eprintln!("failed to create probe runtime: {error}");
process::exit(1);
}
};
match runtime.block_on(run_probe()) {
Ok(()) => println!("ok"),
Err(error) => {
eprintln!("{error}");
process::exit(1);
}
}
}
async fn run_probe() -> Result<(), String> {
let proxy_url = env::var(PROBE_PROXY_ENV).ok();
let target_url = env::var(PROBE_URL_ENV).ok();
let mut builder = reqwest::Client::builder();
if target_url.is_some() {
builder = builder.timeout(Duration::from_secs(5));
}
if env::var_os(PROBE_TLS13_ENV).is_some() {
builder = builder.min_tls_version(reqwest::tls::Version::TLS_1_3);
}
let client = build_probe_client(builder, proxy_url.as_deref())?;
if let Some(url) = target_url {
post_probe_request(&client, &url).await?;
}
Ok(())
}
fn build_probe_client(
builder: reqwest::ClientBuilder,
proxy_url: Option<&str>,
) -> Result<reqwest::Client, String> {
if let Some(proxy_url) = proxy_url {
let proxy = reqwest::Proxy::https(proxy_url)
.map_err(|error| format!("failed to configure probe proxy {proxy_url}: {error}"))?;
return codex_http_client::build_reqwest_client_with_custom_ca(builder.proxy(proxy))
.map_err(|error| error.to_string());
}
codex_http_client::build_reqwest_client_for_subprocess_tests(builder)
.map_err(|error| error.to_string())
}
async fn post_probe_request(client: &reqwest::Client, url: &str) -> Result<(), String> {
let response = client
.post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body("grant_type=authorization_code&code=test")
.send()
.await
.map_err(|error| format!("probe request failed: {error:?}"))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|error| format!("failed to read probe response body: {error}"))?;
if !status.is_success() {
return Err(format!("probe request returned {status}: {body}"));
}
if body != "ok" {
return Err(format!("probe response body mismatch: {body}"));
}
Ok(())
}