diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 5e6bf0788..cdbb65aab 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -53,6 +53,7 @@ use tracing_test::traced_test; const MODEL: &str = "gpt-5.3-codex"; const OPENAI_BETA_HEADER: &str = "OpenAI-Beta"; +const USER_AGENT_HEADER: &str = "user-agent"; const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06"; const X_CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id"; const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; @@ -126,6 +127,10 @@ async fn responses_websocket_streams_request() { handshake.header(X_CLIENT_REQUEST_ID_HEADER), Some(harness.conversation_id.to_string()) ); + assert_eq!( + handshake.header(USER_AGENT_HEADER), + Some(codex_login::default_client::get_codex_user_agent()) + ); assert_eq!( body["client_metadata"]["x-codex-installation-id"].as_str(), Some(TEST_INSTALLATION_ID) @@ -197,6 +202,10 @@ async fn responses_websocket_reuses_connection_with_per_turn_trace_payloads() { }; assert_eq!(server.handshakes().len(), 1); + assert_eq!( + server.single_handshake().header(USER_AGENT_HEADER), + Some(codex_login::default_client::get_codex_user_agent()) + ); let connection = server.single_connection(); assert_eq!(connection.len(), 2); @@ -282,7 +291,12 @@ async fn responses_websocket_preconnect_reuses_connection() { stream_until_complete(&mut client_session, &harness, &prompt).await; assert_eq!(server.handshakes().len(), 1); - assert_eq!(server.single_connection().len(), 1); + assert_eq!( + server.single_handshake().header(USER_AGENT_HEADER), + Some(codex_login::default_client::get_codex_user_agent()) + ); + let connection = server.single_connection(); + assert_eq!(connection.len(), 1); server.shutdown().await; } @@ -315,6 +329,10 @@ async fn responses_websocket_request_prewarm_reuses_connection() { stream_until_complete(&mut client_session, &harness, &prompt).await; assert_eq!(server.handshakes().len(), 1); + assert_eq!( + server.single_handshake().header(USER_AGENT_HEADER), + Some(codex_login::default_client::get_codex_user_agent()) + ); let connection = server.single_connection(); assert_eq!(connection.len(), 2); let warmup = connection @@ -1151,6 +1169,18 @@ async fn responses_websocket_connection_limit_error_reconnects_and_completes() { let total_websocket_requests: usize = server.connections().iter().map(Vec::len).sum(); assert_eq!(total_websocket_requests, 2); + let handshake_user_agents: Vec<_> = server + .handshakes() + .iter() + .map(|handshake| handshake.header(USER_AGENT_HEADER)) + .collect(); + assert_eq!( + handshake_user_agents, + vec![ + Some(codex_login::default_client::get_codex_user_agent()), + Some(codex_login::default_client::get_codex_user_agent()), + ] + ); server.shutdown().await; } diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index 45c80a36d..ee51edf9b 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -12,6 +12,7 @@ use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_terminal_detection::user_agent; use reqwest::header::HeaderMap; use reqwest::header::HeaderValue; +use reqwest::header::USER_AGENT; use std::sync::LazyLock; use std::sync::Mutex; use std::sync::RwLock; @@ -219,12 +220,7 @@ pub fn build_reqwest_client() -> reqwest::Client { /// Callers that need a structured CA-loading failure instead of the legacy logged fallback can use /// this method directly. pub fn try_build_reqwest_client() -> Result { - let ua = get_codex_user_agent(); - - let mut builder = reqwest::Client::builder() - // Set UA via dedicated helper to avoid header validation pitfalls - .user_agent(ua) - .default_headers(default_headers()); + let mut builder = reqwest::Client::builder().default_headers(default_headers()); if is_sandboxed() { builder = builder.no_proxy(); } @@ -236,6 +232,9 @@ pub fn try_build_reqwest_client() -> Result HeaderMap { let mut headers = HeaderMap::new(); headers.insert("originator", originator().header_value); + if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent()) { + headers.insert(USER_AGENT, user_agent); + } if let Ok(guard) = REQUIREMENTS_RESIDENCY.read() && let Some(requirement) = guard.as_ref() && !headers.contains_key(RESIDENCY_HEADER_NAME)