fix(http): preserve native WebSocket trust and optional protocols

This commit is contained in:
Devin AI 2026-10-28 06:02:19 +00:00
parent af811e2fc6
commit 49728dd028
8 changed files with 60 additions and 3 deletions

View file

@ -3980,6 +3980,7 @@ dependencies = [
"reqwest",
"rstest",
"rustls 0.23.42",
"rustls-native-certs",
"serde",
"serde_json",
"strum",

View file

@ -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"] }

View file

@ -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

View file

@ -16,6 +16,7 @@ pub enum Verify {
Disabled,
CaBundle(PathBuf),
BuiltInRoots,
NativeRoots,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]

View file

@ -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(),

View file

@ -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)?)
}

View file

@ -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()

View file

@ -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"),