refactor(rust): resolve the http client config through From and TryFrom

HttpClientConfig::resolve becomes From<&HttpSettings> for Resolution and client_builder becomes TryFrom<&HttpClientConfig> for reqwest::ClientBuilder, matching the rustls conversion. The verify decision moves into From<&HttpSettings> for Verify, and the proxy environment rule moves next to its flags as HttpSettings::trusts_proxy_env. The curve and cipher results are read with transpose and a default selection, which removes the tuple destructuring
This commit is contained in:
Yujong Lee 2026-09-18 19:47:10 -07:00
parent ffbfe7205f
commit 80dbb2a28a
8 changed files with 78 additions and 72 deletions

View file

@ -6,7 +6,7 @@ use litellm_host::{
host::{Host, HostOp, HostResult},
machine::{HostFailure, Machine, MachineStep},
};
use litellm_http::{HttpClientConfig, HttpClientPool, HttpSettings};
use litellm_http::{HttpClientPool, HttpSettings, Resolution};
use litellm_llms::{
base_llm::ocr::{
error::Error as OcrError,
@ -184,7 +184,7 @@ async fn ocr_client_uses_the_injected_http_pool_configuration() {
};
let client = OcrClient::new(
&HttpClientPool::new(Arc::new(PublicDnsResolver)),
&HttpClientConfig::resolve(&settings).config,
&Resolution::from(&settings).config,
UrlPolicy::default(),
VertexAuth::default(),
)

View file

@ -0,0 +1 @@
- https://github.com/BerriAI/litellm-docs/blob/main/docs/guides/security_settings.md

View file

@ -38,84 +38,80 @@ pub struct Resolution {
pub unsupported: Vec<Unsupported>,
}
impl HttpClientConfig {
pub fn resolve(settings: &HttpSettings) -> Resolution {
let (key_exchange_group, unsupported_curve) = match settings
.ssl_ecdh_curve
.as_deref()
.map(str::parse::<KeyExchangeGroup>)
{
None => (None, None),
Some(Ok(group)) => (Some(group), None),
Some(Err(unsupported)) => (None, Some(unsupported)),
};
let ciphers = settings
.ssl_security_level
.as_deref()
.map(CipherSelection::from);
let verify = match &settings.ssl_verify {
Some(SslVerify::Disabled) => Verify::Disabled,
Some(SslVerify::CaBundle(path)) => Verify::CaBundle(path.clone()),
impl From<&HttpSettings> for Verify {
fn from(settings: &HttpSettings) -> Self {
match &settings.ssl_verify {
Some(SslVerify::Disabled) => Self::Disabled,
Some(SslVerify::CaBundle(path)) => Self::CaBundle(path.clone()),
Some(SslVerify::Enabled) | None => settings
.ssl_cert_file
.clone()
.map_or(Verify::BuiltInRoots, Verify::CaBundle),
};
let (tls12_cipher_suites, unsupported_ciphers) = ciphers
.map_or((None, Vec::new()), |ciphers| {
(ciphers.tls12_cipher_suites, ciphers.unsupported)
});
Resolution {
config: Self {
verify,
.map_or(Self::BuiltInRoots, Self::CaBundle),
}
}
}
impl From<&HttpSettings> for Resolution {
fn from(settings: &HttpSettings) -> Self {
let curve = settings
.ssl_ecdh_curve
.as_deref()
.map(str::parse::<KeyExchangeGroup>)
.transpose();
let ciphers = settings
.ssl_security_level
.as_deref()
.map(CipherSelection::from)
.unwrap_or_default();
Self {
config: HttpClientConfig {
verify: Verify::from(settings),
client_certificate: settings.ssl_certificate.clone(),
key_exchange_group,
tls12_cipher_suites,
key_exchange_group: curve.clone().ok().flatten(),
tls12_cipher_suites: ciphers.tls12_cipher_suites,
force_ipv4: settings.force_ipv4,
http2: settings.http2,
user_agent: settings.user_agent.clone(),
trust_proxy_env: !settings.ignore_proxy_env
|| settings.trust_proxy_env
|| settings.http2
|| settings.httpx_transport,
trust_proxy_env: settings.trusts_proxy_env(),
connect_timeout: settings.connect_timeout,
tcp_keepalive: settings.tcp_keepalive,
pool_idle_timeout: settings.pool_idle_timeout,
},
unsupported: unsupported_curve
.into_iter()
.chain(unsupported_ciphers)
.collect(),
unsupported: curve.err().into_iter().chain(ciphers.unsupported).collect(),
}
}
}
pub fn client_builder(&self) -> Result<reqwest::ClientBuilder, Error> {
impl TryFrom<&HttpClientConfig> for reqwest::ClientBuilder {
type Error = Error;
fn try_from(config: &HttpClientConfig) -> Result<Self, Self::Error> {
let base = reqwest::Client::builder()
.use_preconfigured_tls(rustls::ClientConfig::try_from(self)?)
.connect_timeout(self.connect_timeout)
.pool_idle_timeout(self.pool_idle_timeout);
let with_keepalive = match self.tcp_keepalive {
.use_preconfigured_tls(rustls::ClientConfig::try_from(config)?)
.connect_timeout(config.connect_timeout)
.pool_idle_timeout(config.pool_idle_timeout);
let with_keepalive = match config.tcp_keepalive {
None => base,
Some(keepalive) => base
.tcp_keepalive(keepalive.idle)
.tcp_keepalive_interval(keepalive.interval)
.tcp_keepalive_retries(keepalive.retries),
};
let with_address = if self.force_ipv4 {
let with_address = if config.force_ipv4 {
with_keepalive.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
} else {
with_keepalive
};
let with_protocol = if self.http2 {
let with_protocol = if config.http2 {
with_address
} else {
with_address.http1_only()
};
let with_agent = match &self.user_agent {
let with_agent = match &config.user_agent {
Some(agent) => with_protocol.user_agent(agent),
None => with_protocol,
};
Ok(if self.trust_proxy_env {
Ok(if config.trust_proxy_env {
with_agent
} else {
with_agent.no_proxy()
@ -161,7 +157,7 @@ mod tests {
#[case] settings: HttpSettings,
#[case] expected: Verify,
) {
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(config.verify, expected);
}
@ -172,7 +168,7 @@ mod tests {
..HttpSettings::default()
}
.with_environment(&|name: &str| (name == "SSL_VERIFY").then(|| "true".to_string()));
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(config.verify, Verify::BuiltInRoots);
}
@ -188,7 +184,7 @@ mod tests {
ssl_ecdh_curve: Some(curve.into()),
..HttpSettings::default()
};
let resolution = HttpClientConfig::resolve(&settings);
let resolution = Resolution::from(&settings);
assert_eq!(resolution.config.key_exchange_group, expected);
assert_eq!(resolution.unsupported, []);
}
@ -199,7 +195,7 @@ mod tests {
ssl_ecdh_curve: Some("secp521r1".into()),
..HttpSettings::default()
};
let resolution = HttpClientConfig::resolve(&settings);
let resolution = Resolution::from(&settings);
assert_eq!(resolution.config.key_exchange_group, None);
assert_eq!(
resolution.unsupported,
@ -213,7 +209,7 @@ mod tests {
ssl_security_level: Some("DEFAULT@SECLEVEL=1".into()),
..HttpSettings::default()
};
let resolution = HttpClientConfig::resolve(&settings);
let resolution = Resolution::from(&settings);
assert_eq!(resolution.config.tls12_cipher_suites, None);
assert_eq!(
resolution.unsupported,
@ -230,7 +226,7 @@ mod tests {
),
..HttpSettings::default()
};
let resolution = HttpClientConfig::resolve(&settings);
let resolution = Resolution::from(&settings);
assert_eq!(
resolution.config.tls12_cipher_suites,
Some(vec![
@ -265,7 +261,7 @@ mod tests {
pool_idle_timeout: Duration::from_secs(45),
..HttpSettings::default()
};
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(
config,
HttpClientConfig {
@ -303,7 +299,7 @@ mod tests {
#[case] settings: HttpSettings,
#[case] expected: bool,
) {
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(config.trust_proxy_env, expected);
}
@ -312,10 +308,10 @@ mod tests {
let path = std::env::temp_dir().join("litellm-http-missing-bundle.pem");
let config = HttpClientConfig {
verify: Verify::CaBundle(path.clone()),
..HttpClientConfig::resolve(&HttpSettings::default()).config
..Resolution::from(&HttpSettings::default()).config
};
assert!(matches!(
config.client_builder(),
reqwest::ClientBuilder::try_from(&config),
Err(Error::Read { path: reported, .. }) if reported == path
));
}
@ -327,9 +323,9 @@ mod tests {
std::fs::write(&path, b"not a certificate").unwrap();
let config = HttpClientConfig {
verify: Verify::CaBundle(path.clone()),
..HttpClientConfig::resolve(&HttpSettings::default()).config
..Resolution::from(&HttpSettings::default()).config
};
let result = config.client_builder().map(drop);
let result = reqwest::ClientBuilder::try_from(&config).map(drop);
std::fs::remove_file(&path).unwrap();
assert!(matches!(
result,

View file

@ -67,7 +67,9 @@ impl HttpClientPool {
{
return Ok(pooled.client.clone());
}
let client = self.apply(variant, key.0.client_builder()?).build()?;
let client = self
.apply(variant, reqwest::ClientBuilder::try_from(&key.0)?)
.build()?;
self.lock().insert(
key,
PooledClient {
@ -114,7 +116,7 @@ mod tests {
};
use super::*;
use crate::{HttpSettings, Verify};
use crate::{HttpSettings, Resolution, Verify};
struct FixedResolver(SocketAddr);
@ -132,7 +134,7 @@ mod tests {
fn config(user_agent: &str) -> HttpClientConfig {
HttpClientConfig {
user_agent: Some(user_agent.into()),
..HttpClientConfig::resolve(&HttpSettings::default()).config
..Resolution::from(&HttpSettings::default()).config
}
}

View file

@ -111,6 +111,10 @@ impl HttpSettings {
}
}
pub fn trusts_proxy_env(&self) -> bool {
!self.ignore_proxy_env || self.trust_proxy_env || self.http2 || self.httpx_transport
}
pub fn without_missing_files(self, exists: &dyn Fn(&Path) -> bool) -> Self {
Self {
ssl_verify: match self.ssl_verify {

View file

@ -97,6 +97,7 @@ pub enum Unsupported {
CipherToken(String),
}
#[derive(Default)]
pub(crate) struct CipherSelection {
pub(crate) tls12_cipher_suites: Option<Vec<Tls12CipherSuite>>,
pub(crate) unsupported: Vec<Unsupported>,
@ -304,10 +305,10 @@ mod tests {
use rustls::NamedGroup;
use super::*;
use crate::HttpSettings;
use crate::{HttpSettings, Resolution};
fn config(settings: HttpSettings) -> HttpClientConfig {
HttpClientConfig::resolve(&settings).config
Resolution::from(&settings).config
}
fn offered_groups(tls: &ClientConfig) -> Vec<NamedGroup> {

View file

@ -350,7 +350,7 @@ impl Resolve for PublicDnsResolver {
mod tests {
use std::collections::HashSet;
use litellm_http::HttpSettings;
use litellm_http::{HttpSettings, Resolution};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpListener,
@ -445,7 +445,7 @@ mod tests {
) -> MediaFetcher {
let direct = HttpClientConfig {
trust_proxy_env: false,
..HttpClientConfig::resolve(&HttpSettings::default()).config
..Resolution::from(&HttpSettings::default()).config
};
MediaFetcher::with_resolution(
&HttpClientPool::new(Arc::new(LoopbackDnsResolver(pinned_address))),
@ -636,7 +636,7 @@ mod tests {
async fn rejects_url_credentials_before_network_access() {
let fetcher = MediaFetcher::new(
&HttpClientPool::new(Arc::new(PublicDnsResolver)),
&HttpClientConfig::resolve(&HttpSettings::default()).config,
&Resolution::from(&HttpSettings::default()).config,
UrlPolicy::default(),
)
.expect("media fetcher builds");

View file

@ -4,7 +4,9 @@ use std::{
sync::{Arc, LazyLock, Mutex, PoisonError},
};
use litellm_http::{HttpClientConfig, HttpClientPool, HttpSettings, SslVerify, Unsupported};
use litellm_http::{
HttpClientConfig, HttpClientPool, HttpSettings, Resolution, SslVerify, Unsupported,
};
use litellm_llms::custom_httpx::media::{PublicDnsResolver, UrlPolicy};
use pyo3::{prelude::*, types::PyDict};
@ -28,7 +30,7 @@ pub(crate) fn call_config(
.with_environment(&|name| std::env::var(name).ok());
let settings = for_call(configured, call_ssl_verify(kwargs)?, asynchronous)
.without_missing_files(&|path: &Path| path.exists());
let resolution = HttpClientConfig::resolve(&settings);
let resolution = Resolution::from(&settings);
for unsupported in unreported(&REPORTED_UNSUPPORTED, resolution.unsupported) {
PythonSettings::warn(py, &unsupported.to_string())?;
}
@ -251,7 +253,7 @@ user_agent='litellm/9.9.9',
Python::initialize();
Python::attach(|py| {
let settings = settings(&python_settings(py, overrides)).unwrap();
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(config.verify, expected);
});
}
@ -345,7 +347,7 @@ user_agent='litellm/9.9.9',
..HttpSettings::default()
};
let settings = for_call(opted_out, None, asynchronous);
let config = HttpClientConfig::resolve(&settings).config;
let config = Resolution::from(&settings).config;
assert_eq!(config.trust_proxy_env, expected);
}
}