Add system proxy fallback for login and startup requests (#46562)

## Why

Login and enterprise configuration bootstrap can fail when their endpoints are reachable only through the system proxy, before cloud configuration can enable `respect_system_proxy`.

## What changed

- Enable `features.system_proxy_fallback` by default, honor managed feature requirements, and prevent project configuration from changing it.
- Retry account discovery and cloud configuration GETs through the system proxy after connection failures or timeouts, bounding the initial request and body read to five seconds.
- Retry OAuth authorization-code exchange only on connection failures before any redirect, with a ten-second connection timeout. Reuse the successful client for the subsequent API-key exchange to avoid changing routes.
- Increase workspace discovery and cloud configuration timeouts to 15 and 20 seconds, respectively, to accommodate fallback.

## Testing

Add coverage for proxy-only browser login through the first completed turn, including cloud configuration enabling `respect_system_proxy` without a restart. Cover stalled GET bodies, preserved request headers, disabled fallback, managed requirements, and project configuration restrictions. Verify that OAuth HTTP errors, redirects followed by connection failures, and failures after sending the POST do not trigger proxy retries, while successful redirects remain supported.

GitOrigin-RevId: 854eeeac186bb4865218ce645019c1e4bff85425
This commit is contained in:
Abhinav
2026-09-19 01:37:14 +00:00
committed by copyberry
parent 888be42a20
commit 3bb0a530d1
23 changed files with 1134 additions and 71 deletions
@@ -7,6 +7,8 @@
use http::HeaderMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
@@ -31,6 +33,7 @@ use crate::with_chatgpt_cloudflare_cookie_store;
pub struct HttpClientBuilder {
default_headers: Option<HeaderMap>,
follow_redirects: bool,
redirect_observed: Option<Arc<AtomicBool>>,
connect_timeout: Option<Duration>,
chatgpt_cloudflare_cookie_store: bool,
chatgpt_cookie_store: Option<Arc<ChatGptCookieStore>>,
@@ -91,6 +94,13 @@ impl HttpClientBuilder {
self
}
/// Marks the supplied flag when a redirect is encountered, preserving the default policy.
/// Use a fresh flag for each operation whose retry safety depends on its redirect history.
pub fn with_redirect_tracking(mut self, redirect_observed: Arc<AtomicBool>) -> Self {
self.redirect_observed = Some(redirect_observed);
self
}
pub(crate) fn follows_redirects(&self) -> bool {
self.follow_redirects
}
@@ -284,6 +294,11 @@ impl HttpClientBuilder {
}
if !self.follow_redirects {
builder = builder.redirect(reqwest::redirect::Policy::none());
} else if let Some(redirect_observed) = self.redirect_observed {
builder = builder.redirect(reqwest::redirect::Policy::custom(move |attempt| {
redirect_observed.store(/*val*/ true, Ordering::Relaxed);
reqwest::redirect::Policy::default().redirect(attempt)
}));
}
if let Some(connect_timeout) = self.connect_timeout {
builder = builder.connect_timeout(connect_timeout);
@@ -303,6 +318,7 @@ impl Default for HttpClientBuilder {
Self {
default_headers: None,
follow_redirects: true,
redirect_observed: None,
connect_timeout: None,
chatgpt_cloudflare_cookie_store: false,
chatgpt_cookie_store: None,
@@ -166,6 +166,7 @@ impl fmt::Debug for OutboundProxyRoute {
#[derive(Clone)]
pub struct HttpClientFactory {
outbound_proxy_policy: OutboundProxyPolicy,
system_proxy_fallback: bool,
chatgpt_cookie_store: Option<Arc<ChatGptCookieStore>>,
network_policy: NetworkPolicy,
}
@@ -173,6 +174,7 @@ pub struct HttpClientFactory {
impl PartialEq for HttpClientFactory {
fn eq(&self, other: &Self) -> bool {
self.outbound_proxy_policy == other.outbound_proxy_policy
&& self.system_proxy_fallback == other.system_proxy_fallback
&& self.network_policy == other.network_policy
&& self
.chatgpt_cookie_store
@@ -191,6 +193,7 @@ impl fmt::Debug for HttpClientFactory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClientFactory")
.field("outbound_proxy_policy", &self.outbound_proxy_policy)
.field("system_proxy_fallback", &self.system_proxy_fallback)
.field("network_policy", &self.network_policy)
.finish()
}
@@ -201,6 +204,7 @@ impl HttpClientFactory {
pub const fn new(outbound_proxy_policy: OutboundProxyPolicy) -> Self {
Self {
outbound_proxy_policy,
system_proxy_fallback: false,
chatgpt_cookie_store: None,
network_policy: NetworkPolicy::unmanaged(),
}
@@ -216,6 +220,24 @@ impl HttpClientFactory {
&self.network_policy
}
/// Changes the route policy while retaining the configured cookies and network policy.
pub fn with_outbound_proxy_policy(mut self, policy: OutboundProxyPolicy) -> Self {
self.outbound_proxy_policy = policy;
self
}
/// Allows bootstrap callers to retry through the system proxy when their request is safe
/// to replay. This does not change routing or enable retries for ordinary HTTP requests.
pub fn with_system_proxy_fallback(mut self) -> Self {
self.system_proxy_fallback = true;
self
}
pub fn allows_system_proxy_fallback(&self) -> bool {
self.system_proxy_fallback
&& self.outbound_proxy_policy == OutboundProxyPolicy::ReqwestDefault
}
/// Adds process-scoped cookies to requests made by ChatGPT cookie-store clients.
pub fn with_chatgpt_cookies(mut self, cookies: impl IntoIterator<Item = HeaderValue>) -> Self {
let cookies = cookies.into_iter().collect::<Vec<_>>();
@@ -312,6 +312,16 @@ impl RouteAwareClientPool {
self.http_client_factory.outbound_proxy_policy()
}
pub fn allows_system_proxy_fallback(&self) -> bool {
self.http_client_factory.allows_system_proxy_fallback()
}
/// Changes routing while preserving transport settings and clients cached by resolved route.
pub fn with_outbound_proxy_policy(mut self, policy: OutboundProxyPolicy) -> Self {
self.http_client_factory = self.http_client_factory.with_outbound_proxy_policy(policy);
self
}
/// Creates a pool with the shared default HTTP transport settings.
pub fn new(http_client_factory: HttpClientFactory, route_class: ClientRouteClass) -> Self {
Self::with_builder(http_client_factory, route_class, HttpClientBuilder::new())
@@ -125,6 +125,7 @@ fn request_builder_debug_redacts_url_secrets() {
concat!(
"RouteAwareRequestBuilder { pool: RouteAwareClientPool { ",
"http_client_factory: HttpClientFactory { outbound_proxy_policy: ReqwestDefault, ",
"system_proxy_fallback: false, ",
"network_policy: NetworkPolicy { managed: false, .. } }, ",
"route_class: Api, .. }, method: Some(GET), ",
"url: Some(\"<redacted>\"), .. }"