From 49728dd028b8d2b25cf3c6ca8277857325bcb045 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:02:19 +0000 Subject: [PATCH] fix(http): preserve native WebSocket trust and optional protocols --- litellm-rust/Cargo.lock | 1 + litellm-rust/Cargo.toml | 1 + litellm-rust/crates/http/Cargo.toml | 1 + litellm-rust/crates/http/src/config.rs | 1 + litellm-rust/crates/http/src/pool.rs | 11 +++++- litellm-rust/crates/http/src/tls.rs | 9 +++++ litellm-rust/crates/http/src/websocket.rs | 2 +- litellm-rust/crates/http/tests/websocket.rs | 37 ++++++++++++++++++++- 8 files changed, 60 insertions(+), 3 deletions(-) diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index 1fdd1ae6f18..71ed05c0adf 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -3980,6 +3980,7 @@ dependencies = [ "reqwest", "rstest", "rustls 0.23.42", + "rustls-native-certs", "serde", "serde_json", "strum", diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml index ce0719af922..745b29dd0ff 100644 --- a/litellm-rust/Cargo.toml +++ b/litellm-rust/Cargo.toml @@ -100,6 +100,7 @@ rstest = "0.26.1" wiremock = "0.6.5" rstest_reuse = "0.7.0" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +rustls-native-certs = "0.8" rustify = "=0.7.0" rustify_derive = "=0.5.5" vaultrs = { version = "=0.8.0", default-features = false, features = ["rustls"] } diff --git a/litellm-rust/crates/http/Cargo.toml b/litellm-rust/crates/http/Cargo.toml index f787192f4c4..0552f9775fd 100644 --- a/litellm-rust/crates/http/Cargo.toml +++ b/litellm-rust/crates/http/Cargo.toml @@ -14,6 +14,7 @@ litellm-core-utils.workspace = true hyper-util.workspace = true reqwest.workspace = true rustls.workspace = true +rustls-native-certs.workspace = true tokio-tungstenite.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/litellm-rust/crates/http/src/config.rs b/litellm-rust/crates/http/src/config.rs index 6a2d80e3f85..b37ea6559b8 100644 --- a/litellm-rust/crates/http/src/config.rs +++ b/litellm-rust/crates/http/src/config.rs @@ -16,6 +16,7 @@ pub enum Verify { Disabled, CaBundle(PathBuf), BuiltInRoots, + NativeRoots, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] diff --git a/litellm-rust/crates/http/src/pool.rs b/litellm-rust/crates/http/src/pool.rs index dc96e6713dd..bf892b09921 100644 --- a/litellm-rust/crates/http/src/pool.rs +++ b/litellm-rust/crates/http/src/pool.rs @@ -6,7 +6,12 @@ use std::{ use reqwest::dns::Resolve; -use crate::{client::Client, config::HttpClientConfig, error::Error, proxy::EnvironmentProxies}; +use crate::{ + client::Client, + config::{HttpClientConfig, Verify}, + error::Error, + proxy::EnvironmentProxies, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ClientVariant { @@ -62,6 +67,10 @@ impl HttpClientPool { }, ClientVariant::WebSocket => HttpClientConfig { http2: false, + verify: match &config.verify { + Verify::BuiltInRoots => Verify::NativeRoots, + verify => verify.clone(), + }, ..config.clone() }, ClientVariant::Provider | ClientVariant::NoRedirect => config.clone(), diff --git a/litellm-rust/crates/http/src/tls.rs b/litellm-rust/crates/http/src/tls.rs index 0b1c44bf6fe..eab38875088 100644 --- a/litellm-rust/crates/http/src/tls.rs +++ b/litellm-rust/crates/http/src/tls.rs @@ -195,6 +195,15 @@ impl TryFrom<&HttpClientConfig> for ClientConfig { Verify::BuiltInRoots => builder.with_root_certificates(RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), }), + Verify::NativeRoots => { + let native = rustls_native_certs::load_native_certs(); + let mut roots = RootCertStore::empty(); + roots.add_parsable_certificates(native.certs); + if roots.is_empty() { + return Err(Error::Client("no usable native root certificates".into())); + } + builder.with_root_certificates(roots) + } Verify::CaBundle(path) => { builder.with_root_certificates(bundle_roots(path, TlsSource::CaBundle)?) } diff --git a/litellm-rust/crates/http/src/websocket.rs b/litellm-rust/crates/http/src/websocket.rs index d9ce183201d..685c79cf3a1 100644 --- a/litellm-rust/crates/http/src/websocket.rs +++ b/litellm-rust/crates/http/src/websocket.rs @@ -79,7 +79,7 @@ fn validate_response_headers( )); } let protocol_matches = match (offered_protocols, headers.get(SEC_WEBSOCKET_PROTOCOL)) { - (None, None) => true, + (_, None) => true, (Some(offered), Some(selected)) => offered .to_str() .ok() diff --git a/litellm-rust/crates/http/tests/websocket.rs b/litellm-rust/crates/http/tests/websocket.rs index 03bf6c956a7..f2244587a04 100644 --- a/litellm-rust/crates/http/tests/websocket.rs +++ b/litellm-rust/crates/http/tests/websocket.rs @@ -274,6 +274,41 @@ async fn wss_honors_ssl_verify(#[case] verify: fn(&Pki) -> Verify, #[case] trust } } +#[rstest] +#[tokio::test] +async fn wss_uses_system_roots() { + const CHILD_URL: &str = "LITELLM_TEST_SYSTEM_ROOTS_WSS_URL"; + if let Ok(url) = std::env::var(CHILD_URL) { + let mut socket = open(&config(Verify::BuiltInRoots), &url, HeaderMap::new()) + .await + .unwrap(); + round_trip(&mut socket).await; + return; + } + + let pki = Pki::new(); + let (address, _) = serve_tls(&pki, false).await; + let url = format!("wss://localhost:{}/responses", address.port()); + let ca = pki.path("ca.pem"); + let output = tokio::task::spawn_blocking(move || { + std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "wss_uses_system_roots", "--nocapture"]) + .env(CHILD_URL, url) + .env("SSL_CERT_FILE", ca) + .env("SSL_CERT_DIR", "") + .output() + .unwrap() + }) + .await + .unwrap(); + assert!( + output.status.success(), + "system-root WSS child failed:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + #[rstest] #[case::with_certificate(true)] #[case::without_certificate(false)] @@ -390,7 +425,7 @@ async fn invalid_url_is_rejected_before_dispatch(#[case] url: &str) { Some("other"), false )] -#[case::missing_protocol(Some("websocket"), Some("Upgrade"), Some("events"), None, false)] +#[case::declined_optional_protocol(Some("websocket"), Some("Upgrade"), Some("events"), None, true)] #[case::matching_protocol( Some("websocket"), Some("Upgrade"),