From 94cecddf8a55dbf999ff9c024c61deb4980eb176 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 6 Jul 2026 20:33:22 -0700 Subject: [PATCH] core: use HTTP Responses with system proxy --- codex-rs/core/src/client.rs | 10 +++- .../core/tests/suite/websocket_fallback.rs | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d5918d5209..5255b3e024 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -64,6 +64,7 @@ use codex_api::create_text_param_for_request; use codex_api::response_create_client_metadata; use codex_http_client::ClientRouteClass; use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::RefreshTokenError; @@ -912,9 +913,14 @@ impl ModelClient { /// Returns whether the Responses-over-WebSocket transport is active for this session. /// - /// WebSocket use is controlled by provider capability and session-scoped fallback state. + /// WebSocket use is controlled by outbound proxy policy, provider capability, and + /// session-scoped fallback state. System-proxy sessions conservatively use HTTP until the + /// WebSocket transport supports proxy-aware dialing. pub fn responses_websocket_enabled(&self) -> bool { - if !self.state.provider.info().supports_websockets + if matches!( + self.http_client_factory.outbound_proxy_policy(), + OutboundProxyPolicy::RespectSystemProxy + ) || !self.state.provider.info().supports_websockets || self.state.disable_websockets.load(Ordering::Relaxed) { return false; diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index cdf77694c2..c7923fe260 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use codex_config::CONFIG_TOML_FILE; use codex_model_provider_info::WireApi; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; @@ -26,6 +27,60 @@ use wiremock::http::Method; use wiremock::matchers::method; use wiremock::matchers::path_regex; +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn system_proxy_policy_uses_http_without_websocket_handshake() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let response_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let mut builder = test_codex() + .with_pre_build_hook(|codex_home| { + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + "[features]\nrespect_system_proxy = true\n", + ) + .expect("system-proxy feature config should be written"); + }) + .with_config({ + let base_url = format!("{}/v1", server.uri()); + move |config| { + config.model_provider.base_url = Some(base_url); + config.model_provider.wire_api = WireApi::Responses; + config.model_provider.supports_websockets = true; + config.model_provider.request_max_retries = Some(0); + } + }); + let test = builder.build_with_auto_env(&server).await?; + assert!(test.config.respect_system_proxy); + + test.submit_turn("hello").await?; + + let requests = server.received_requests().await.unwrap_or_default(); + let websocket_attempts = requests + .iter() + .filter(|request| { + request.method == Method::GET && request.url.path().ends_with("/responses") + }) + .count(); + let http_attempts = requests + .iter() + .filter(|request| { + request.method == Method::POST && request.url.path().ends_with("/responses") + }) + .count(); + + assert_eq!(websocket_attempts, 0); + assert_eq!(http_attempts, 1); + assert_eq!(response_mock.requests().len(), 1); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn websocket_fallback_switches_to_http_on_upgrade_required_connect() -> Result<()> { skip_if_no_network!(Ok(()));