Add rustls fallback for local MCP HTTP requests (#38436)

## Why

Local MCP requests can fail when the platform TLS backend cannot negotiate a
protocol version with an HTTPS endpoint.

## What changed

- Retry replayable local MCP requests once with rustls after a recognized TLS
  protocol-version negotiation failure. Keep certificate, timeout, and unrelated
  connection failures on the existing error path.
- Remember successful fallback per HTTPS origin and outbound route, while keeping
  the platform TLS backend as the default for other destinations.
- Share the fallback-enabled client across local MCP resolution, CLI login, and
  OAuth discovery while preserving remote environment HTTP clients.

## Testing

Added coverage for platform-specific error detection, request replay, cached
fallback reuse and isolation, non-replayable requests, redirects, and remote MCP
client selection.

GitOrigin-RevId: 39a2d96fdb2ea0e51df14f652ba2a953d24e69a1
This commit is contained in:
Celia Chen authored and copyberry committed 2026-08-13 21:13:07 +00:00
1 parent 93327c852a
commit b87327f4e5
10 files changed
+1204 -30

No files matched your search

@@ -9,6 +9,8 @@ use http::HeaderMap;
use std::sync::Arc;
use std::time::Duration;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use crate::BuildCustomCaTransportError;
use crate::BuildRouteAwareHttpClientError;
use crate::ClientRouteClass;
@@ -33,6 +35,14 @@ pub struct HttpClientBuilder {
chatgpt_cloudflare_cookie_store: bool,
chatgpt_cookie_store: Option<Arc<ChatGptCookieStore>>,
request_logging: RequestLogging,
tls_backend: TlsBackend,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum TlsBackend {
#[default]
TransportDefault,
Rustls,
}
impl HttpClientFactory {
@@ -85,6 +95,11 @@ impl HttpClientBuilder {
self.follow_redirects
}
pub(crate) fn with_rustls_tls(mut self) -> Self {
self.tls_backend = TlsBackend::Rustls;
self
}
/// Limits only connection establishment, not the request as a whole.
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
@@ -260,6 +275,10 @@ impl HttpClientBuilder {
fn base_reqwest_builder(self) -> reqwest::ClientBuilder {
let mut builder = reqwest::Client::builder();
if self.tls_backend == TlsBackend::Rustls {
ensure_rustls_crypto_provider();
builder = builder.use_rustls_tls();
}
if let Some(default_headers) = self.default_headers {
builder = builder.default_headers(default_headers);
}
@@ -288,6 +307,7 @@ impl Default for HttpClientBuilder {
chatgpt_cloudflare_cookie_store: false,
chatgpt_cookie_store: None,
request_logging: RequestLogging::Enabled,
tls_backend: TlsBackend::TransportDefault,
}
}
}