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
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-07-07 01:12:24 +00:00
1 parent 45be435135
commit 9acfe8965d
30 files changed
+187 -128

No files matched your search

@@ -0,0 +1,384 @@
//! macOS system proxy resolution through SystemConfiguration and CFNetwork.
use std::ffi::c_void;
use std::ptr;
use std::time::Duration;
use std::time::Instant;
use super::RequestOrigin;
use super::RouteFailureClass;
use super::SystemProxyDecision;
use system_configuration::core_foundation::array::CFArray;
use system_configuration::core_foundation::array::CFArrayRef;
use system_configuration::core_foundation::base::CFEqual;
use system_configuration::core_foundation::base::CFGetTypeID;
use system_configuration::core_foundation::base::CFIndex;
use system_configuration::core_foundation::base::CFType;
use system_configuration::core_foundation::base::CFTypeRef;
use system_configuration::core_foundation::base::TCFType;
use system_configuration::core_foundation::base::kCFAllocatorDefault;
use system_configuration::core_foundation::dictionary::CFDictionary;
use system_configuration::core_foundation::dictionary::CFDictionaryRef;
use system_configuration::core_foundation::error::CFErrorRef;
use system_configuration::core_foundation::number::CFNumber;
use system_configuration::core_foundation::runloop::CFRunLoop;
use system_configuration::core_foundation::runloop::CFRunLoopSource;
use system_configuration::core_foundation::runloop::CFRunLoopSourceInvalidate;
use system_configuration::core_foundation::runloop::CFRunLoopSourceRef;
use system_configuration::core_foundation::runloop::kCFRunLoopDefaultMode;
use system_configuration::core_foundation::string::CFString;
use system_configuration::core_foundation::string::CFStringRef;
use system_configuration::core_foundation::url::CFURL;
use system_configuration::core_foundation::url::CFURLCreateWithString;
use system_configuration::core_foundation::url::CFURLGetTypeID;
use system_configuration::core_foundation::url::CFURLRef;
use system_configuration::dynamic_store::SCDynamicStoreBuilder;
const PAC_EXECUTION_TIMEOUT: Duration = Duration::from_secs(5);
type ProxyDictionary = CFDictionary<CFString, CFType>;
type ProxyArray = CFArray<ProxyDictionary>;
#[repr(C)]
struct CFStreamClientContext {
version: CFIndex,
info: *mut c_void,
retain: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
release: Option<unsafe extern "C" fn(*mut c_void)>,
copy_description: Option<unsafe extern "C" fn(*mut c_void) -> CFStringRef>,
}
type CFProxyAutoConfigurationResultCallback =
unsafe extern "C" fn(*mut c_void, CFArrayRef, CFErrorRef);
#[link(name = "CFNetwork", kind = "framework")]
unsafe extern "C" {
static kCFProxyTypeKey: CFStringRef;
static kCFProxyHostNameKey: CFStringRef;
static kCFProxyPortNumberKey: CFStringRef;
static kCFProxyAutoConfigurationURLKey: CFStringRef;
static kCFProxyAutoConfigurationJavaScriptKey: CFStringRef;
static kCFProxyTypeNone: CFStringRef;
static kCFProxyTypeHTTP: CFStringRef;
static kCFProxyTypeHTTPS: CFStringRef;
static kCFProxyTypeSOCKS: CFStringRef;
static kCFProxyTypeAutoConfigurationURL: CFStringRef;
static kCFProxyTypeAutoConfigurationJavaScript: CFStringRef;
fn CFNetworkCopyProxiesForURL(url: CFURLRef, proxy_settings: CFDictionaryRef) -> CFArrayRef;
fn CFNetworkExecuteProxyAutoConfigurationURL(
proxy_auto_config_url: CFURLRef,
target_url: CFURLRef,
callback: CFProxyAutoConfigurationResultCallback,
client_context: *mut CFStreamClientContext,
) -> CFRunLoopSourceRef;
fn CFNetworkExecuteProxyAutoConfigurationScript(
proxy_auto_config_script: CFStringRef,
target_url: CFURLRef,
callback: CFProxyAutoConfigurationResultCallback,
client_context: *mut CFStreamClientContext,
) -> CFRunLoopSourceRef;
}
pub(super) fn resolve(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
let Some(target_url) = cf_url(request_url) else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::InvalidProxyConfig,
};
};
let Some(settings) = system_proxy_settings() else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
let Some(proxies) = copy_proxies_for_url(&target_url, &settings) else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
proxy_array_decision(&proxies, &target_url, origin)
}
fn system_proxy_settings() -> Option<CFDictionary<CFString, CFType>> {
let store = SCDynamicStoreBuilder::new("Codex").build()?;
store.get_proxies()
}
fn copy_proxies_for_url(
target_url: &CFURL,
settings: &CFDictionary<CFString, CFType>,
) -> Option<ProxyArray> {
let proxies = unsafe {
CFNetworkCopyProxiesForURL(
target_url.as_concrete_TypeRef(),
settings.as_concrete_TypeRef(),
)
};
if proxies.is_null() {
None
} else {
Some(unsafe { ProxyArray::wrap_under_create_rule(proxies) })
}
}
fn proxy_array_decision(
proxies: &ProxyArray,
target_url: &CFURL,
origin: &RequestOrigin,
) -> SystemProxyDecision {
let mut saw_unsupported = false;
let mut saw_unavailable = false;
// CFNetwork returns candidates in failover order, but the shared resolver currently carries
// only one route. This matches the Windows limitation; cross-platform retry requires request
// replay semantics and is intentionally deferred.
for proxy in proxies {
match proxy_entry_decision(&proxy, target_url, origin) {
ProxyEntryDecision::Direct => return SystemProxyDecision::Direct,
ProxyEntryDecision::Proxy { url } => return SystemProxyDecision::Proxy { url },
ProxyEntryDecision::UnsupportedScheme => saw_unsupported = true,
ProxyEntryDecision::Unavailable => saw_unavailable = true,
}
}
if saw_unsupported {
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
}
} else if saw_unavailable {
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else {
SystemProxyDecision::Direct
}
}
fn proxy_entry_decision(
proxy: &ProxyDictionary,
target_url: &CFURL,
origin: &RequestOrigin,
) -> ProxyEntryDecision {
let Some(proxy_type) = cf_string_value(proxy, unsafe { kCFProxyTypeKey }) else {
return ProxyEntryDecision::Unavailable;
};
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeNone }) {
return ProxyEntryDecision::Direct;
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTP }) {
return concrete_proxy_entry(proxy, "http");
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTPS }) {
// CFNetwork's HTTPS proxy type is a tunneling proxy for HTTPS destinations; it does not
// preserve an explicit TLS-to-proxy transport. See https://developer.apple.com/documentation/cfnetwork/kcfproxytypehttps.
return concrete_proxy_entry(proxy, "http");
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeSOCKS }) {
return ProxyEntryDecision::UnsupportedScheme;
}
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeAutoConfigurationURL }) {
let Some(pac_url) = cf_url_value(proxy, unsafe { kCFProxyAutoConfigurationURLKey }) else {
return ProxyEntryDecision::Unavailable;
};
return pac_decision(execute_pac_url(&pac_url, target_url), target_url, origin);
}
if cf_string_equals(&proxy_type, unsafe {
kCFProxyTypeAutoConfigurationJavaScript
}) {
let Some(script) =
cf_string_value(proxy, unsafe { kCFProxyAutoConfigurationJavaScriptKey })
else {
return ProxyEntryDecision::Unavailable;
};
return pac_decision(
execute_pac(|callback, context| unsafe {
CFNetworkExecuteProxyAutoConfigurationScript(
script.as_concrete_TypeRef(),
target_url.as_concrete_TypeRef(),
callback,
context,
)
}),
target_url,
origin,
);
}
ProxyEntryDecision::Unavailable
}
fn pac_decision(
result: Result<ProxyArray, RouteFailureClass>,
target_url: &CFURL,
origin: &RequestOrigin,
) -> ProxyEntryDecision {
let proxies = match result {
Ok(proxies) => proxies,
Err(RouteFailureClass::UnsupportedProxyScheme) => {
return ProxyEntryDecision::UnsupportedScheme;
}
Err(_) => return ProxyEntryDecision::Unavailable,
};
match proxy_array_decision(&proxies, target_url, origin) {
SystemProxyDecision::Direct => ProxyEntryDecision::Direct,
SystemProxyDecision::Proxy { url } => ProxyEntryDecision::Proxy { url },
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
} => ProxyEntryDecision::UnsupportedScheme,
SystemProxyDecision::Unavailable { failure: _ } => ProxyEntryDecision::Unavailable,
}
}
fn execute_pac_url(pac_url: &CFURL, target_url: &CFURL) -> Result<ProxyArray, RouteFailureClass> {
execute_pac(|callback, context| unsafe {
CFNetworkExecuteProxyAutoConfigurationURL(
pac_url.as_concrete_TypeRef(),
target_url.as_concrete_TypeRef(),
callback,
context,
)
})
}
fn execute_pac(
create_source: impl FnOnce(
CFProxyAutoConfigurationResultCallback,
*mut CFStreamClientContext,
) -> CFRunLoopSourceRef,
) -> Result<ProxyArray, RouteFailureClass> {
let mut state = PacRunLoopState { result: None };
let mut context = CFStreamClientContext {
version: 0,
info: (&mut state as *mut PacRunLoopState).cast::<c_void>(),
retain: None,
release: None,
copy_description: None,
};
let source = create_source(pac_result_callback, &mut context);
if source.is_null() {
return Err(RouteFailureClass::ProxyResolutionUnavailable);
}
let source = unsafe { CFRunLoopSource::wrap_under_create_rule(source) };
let run_loop = CFRunLoop::get_current();
let mode = unsafe { kCFRunLoopDefaultMode };
run_loop.add_source(&source, mode);
let started_at = Instant::now();
while state.result.is_none() && started_at.elapsed() < PAC_EXECUTION_TIMEOUT {
CFRunLoop::run_in_mode(mode, Duration::from_millis(50), true);
}
if state.result.is_none() {
unsafe { CFRunLoopSourceInvalidate(source.as_concrete_TypeRef()) };
}
run_loop.remove_source(&source, mode);
state
.result
.unwrap_or(Err(RouteFailureClass::ConnectTimeout))
}
unsafe extern "C" fn pac_result_callback(
client: *mut c_void,
proxies: CFArrayRef,
error: CFErrorRef,
) {
let state = unsafe { &mut *client.cast::<PacRunLoopState>() };
state.result = if !error.is_null() || proxies.is_null() {
Some(Err(RouteFailureClass::ProxyResolutionUnavailable))
} else {
Some(Ok(unsafe { ProxyArray::wrap_under_get_rule(proxies) }))
};
CFRunLoop::get_current().stop();
}
struct PacRunLoopState {
result: Option<Result<ProxyArray, RouteFailureClass>>,
}
fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEntryDecision {
let Some(host) = cf_string_value(proxy, unsafe { kCFProxyHostNameKey })
.map(|host| host.to_string())
.filter(|host| !host.is_empty())
else {
return ProxyEntryDecision::Unavailable;
};
let host = bracket_ipv6_host(&host);
let url = match cf_i32_value(proxy, unsafe { kCFProxyPortNumberKey }) {
Some(port) if port > 0 => format!("{proxy_scheme}://{host}:{port}"),
_ => format!("{proxy_scheme}://{host}"),
};
ProxyEntryDecision::Proxy { url }
}
fn bracket_ipv6_host(host: &str) -> String {
if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
}
}
fn cf_string_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFString> {
proxy
.find(key)
.and_then(|value| value.downcast::<CFString>())
}
fn cf_i32_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<i32> {
proxy
.find(key)
.and_then(|value| value.downcast::<CFNumber>())
.and_then(|value| value.to_i32())
}
fn cf_url_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFURL> {
proxy.find(key).and_then(|value| {
if unsafe { CFGetTypeID(value.as_CFTypeRef()) == CFURLGetTypeID() } {
Some(unsafe { CFURL::wrap_under_get_rule(value.as_CFTypeRef() as CFURLRef) })
} else {
value
.downcast::<CFString>()
.and_then(|value| cf_url(value.to_string().as_str()))
}
})
}
fn cf_string_equals(value: &CFString, expected: CFStringRef) -> bool {
unsafe { CFEqual(value.as_CFTypeRef(), expected as CFTypeRef) != 0 }
}
fn cf_url(value: &str) -> Option<CFURL> {
let value = CFString::new(value);
let url = unsafe {
CFURLCreateWithString(
kCFAllocatorDefault,
value.as_concrete_TypeRef(),
ptr::null(),
)
};
if url.is_null() {
None
} else {
Some(unsafe { CFURL::wrap_under_create_rule(url) })
}
}
enum ProxyEntryDecision {
Direct,
Proxy { url: String },
UnsupportedScheme,
Unavailable,
}