Respect system proxies during plugin startup sync (#34506)

## Why

The curated plugin startup sync can fall back from Git to HTTP, but those
requests did not use Codex's effective outbound proxy policy.

## What changed

- Thread the configured HTTP client factory into curated plugin sync.
- Route GitHub API, zipball, backup metadata, and returned download URLs
  through the route-aware client when system proxy support is enabled.
- Preserve the existing default HTTP client behavior, including its custom CA
  fallback, when system proxy support is disabled.
- Apply the standard Codex headers to startup-sync HTTP requests.

## Testing

Add coverage that records route selection for repository, archive, metadata,
and backend-supplied download URLs, and verifies standard headers are sent.

GitOrigin-RevId: e1ea13bdab58a84c9948a2c8b82a5ec5c964ea99
This commit is contained in:
Michael Bolin authored and copyberry committed 2026-07-21 09:46:00 +00:00
1 parent cc875d61ce
commit 94bb6a09a6
17 files changed
+453 -87

No files matched your search

@@ -0,0 +1,101 @@
//! Startup-sync-specific HTTP transport selection.
//!
//! Curated plugin startup sync normally uses git, so its HTTP path is also a recovery path for
//! machines where git is unavailable or fails. Under `ReqwestDefault`, that recovery path must
//! preserve the legacy `codex_login::default_client::build_reqwest_client()` behavior: invalid
//! custom-CA configuration is logged and falls back to a normal reqwest client instead of making
//! HTTP sync fail as well.
//!
//! When `RespectSystemProxy` is enabled, however, every concrete request URL—including download
//! URLs returned by another endpoint—must be routed through `RouteAwareClientPool` so PAC and
//! operating-system proxy settings are respected. The route-aware pool intentionally surfaces
//! client-construction errors and therefore cannot provide the legacy custom-CA fallback for free.
//!
//! `StartupSyncHttpClient` keeps those two policies behind one request API without making lenient
//! custom-CA handling a global HTTP-client behavior. This module selects the transport only;
//! startup-sync request helpers remain responsible for applying the standard Codex headers.
use std::sync::Arc;
use std::time::Duration;
use crate::http_client_selector::HttpClientSelector;
use codex_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_http_client::RouteAwareClientPool;
use codex_http_client::RouteAwareRequestBuilder;
use codex_login::default_client::build_reqwest_client;
use http::HeaderMap;
use http::Method;
pub(super) enum StartupSyncHttpClient {
Default(reqwest::Client),
RouteAware(Arc<dyn HttpClientSelector>),
}
impl StartupSyncHttpClient {
pub(super) fn new(http_client_factory: &HttpClientFactory) -> Self {
match http_client_factory.outbound_proxy_policy() {
OutboundProxyPolicy::ReqwestDefault => Self::Default(build_reqwest_client()),
OutboundProxyPolicy::RespectSystemProxy => {
let http_clients =
RouteAwareClientPool::with_chatgpt_cloudflare_cookies_without_request_logging(
http_client_factory.clone(),
ClientRouteClass::Api,
);
Self::RouteAware(Arc::new(http_clients))
}
}
}
#[cfg(test)]
pub(super) fn route_aware(http_clients: Arc<dyn HttpClientSelector>) -> Self {
Self::RouteAware(http_clients)
}
pub(super) fn request(&self, method: Method, url: &str) -> StartupSyncRequestBuilder {
match self {
Self::Default(client) => {
StartupSyncRequestBuilder::Default(client.request(method, url))
}
Self::RouteAware(http_clients) => {
StartupSyncRequestBuilder::RouteAware(http_clients.request(method, url))
}
}
}
}
pub(super) enum StartupSyncRequestBuilder {
Default(reqwest::RequestBuilder),
RouteAware(RouteAwareRequestBuilder),
}
impl StartupSyncRequestBuilder {
pub(super) fn timeout(self, timeout: Duration) -> Self {
match self {
Self::Default(request) => Self::Default(request.timeout(timeout)),
Self::RouteAware(request) => Self::RouteAware(request.timeout(timeout)),
}
}
pub(super) fn header(self, key: &'static str, value: &'static str) -> Self {
match self {
Self::Default(request) => Self::Default(request.header(key, value)),
Self::RouteAware(request) => Self::RouteAware(request.header(key, value)),
}
}
pub(super) fn headers(self, headers: HeaderMap) -> Self {
match self {
Self::Default(request) => Self::Default(request.headers(headers)),
Self::RouteAware(request) => Self::RouteAware(request.headers(headers)),
}
}
pub(super) async fn send(self) -> Result<reqwest::Response, String> {
match self {
Self::Default(request) => request.send().await.map_err(|err| err.to_string()),
Self::RouteAware(request) => request.send().await.map_err(|err| err.to_string()),
}
}
}