diff --git a/litellm-rust/crates/http/src/config.rs b/litellm-rust/crates/http/src/config.rs index 7772e6cce5b..24a52315f7c 100644 --- a/litellm-rust/crates/http/src/config.rs +++ b/litellm-rust/crates/http/src/config.rs @@ -55,7 +55,10 @@ impl HttpClientConfig { force_ipv4: settings.force_ipv4, http2: settings.http2, user_agent: settings.user_agent.clone(), - trust_proxy_env: settings.trust_proxy_env || settings.http2 || settings.httpx_transport, + trust_proxy_env: !settings.ignore_proxy_env + || settings.trust_proxy_env + || settings.http2 + || settings.httpx_transport, connect_timeout: settings.connect_timeout, }) } @@ -237,11 +240,21 @@ mod tests { } #[rstest] - #[case::aiohttp_default(HttpSettings::default(), false)] - #[case::aiohttp_trust_env(HttpSettings { trust_proxy_env: true, ..HttpSettings::default() }, true)] - #[case::http2_uses_httpx(HttpSettings { http2: true, ..HttpSettings::default() }, true)] - #[case::aiohttp_disabled(HttpSettings { httpx_transport: true, ..HttpSettings::default() }, true)] - fn environment_proxies_apply_whenever_python_would_use_httpx( + #[case::aiohttp_default(HttpSettings::default(), true)] + #[case::aiohttp_opted_out(HttpSettings { ignore_proxy_env: true, ..HttpSettings::default() }, false)] + #[case::session_trust_env_beats_opt_out( + HttpSettings { ignore_proxy_env: true, trust_proxy_env: true, ..HttpSettings::default() }, + true + )] + #[case::http2_uses_httpx( + HttpSettings { ignore_proxy_env: true, http2: true, ..HttpSettings::default() }, + true + )] + #[case::aiohttp_disabled( + HttpSettings { ignore_proxy_env: true, httpx_transport: true, ..HttpSettings::default() }, + true + )] + fn environment_proxies_apply_unless_the_aiohttp_transport_opts_out( #[case] settings: HttpSettings, #[case] expected: bool, ) { diff --git a/litellm-rust/crates/http/src/settings.rs b/litellm-rust/crates/http/src/settings.rs index 55ac471bba9..c572c56ef3a 100644 --- a/litellm-rust/crates/http/src/settings.rs +++ b/litellm-rust/crates/http/src/settings.rs @@ -32,6 +32,7 @@ pub struct HttpSettings { pub httpx_transport: bool, pub user_agent: Option, pub trust_proxy_env: bool, + pub ignore_proxy_env: bool, pub connect_timeout: Duration, } @@ -48,6 +49,7 @@ impl Default for HttpSettings { httpx_transport: false, user_agent: None, trust_proxy_env: false, + ignore_proxy_env: false, connect_timeout: Duration::from_secs(5), } } @@ -78,6 +80,7 @@ impl HttpSettings { httpx_transport: self.httpx_transport || enabled("DISABLE_AIOHTTP_TRANSPORT"), user_agent: env("LITELLM_USER_AGENT").or(self.user_agent), trust_proxy_env: self.trust_proxy_env || enabled("AIOHTTP_TRUST_ENV"), + ignore_proxy_env: self.ignore_proxy_env || enabled("DISABLE_AIOHTTP_TRUST_ENV"), ..self } } @@ -214,14 +217,16 @@ mod tests { #[case("1", false)] fn boolean_switches_only_turn_on_for_true(#[case] value: &'static str, #[case] expected: bool) { let env = move |name: &str| match name { - "LITELLM_HTTP2" | "AIOHTTP_TRUST_ENV" | "DISABLE_AIOHTTP_TRANSPORT" => { - Some(value.to_string()) - } + "LITELLM_HTTP2" + | "AIOHTTP_TRUST_ENV" + | "DISABLE_AIOHTTP_TRANSPORT" + | "DISABLE_AIOHTTP_TRUST_ENV" => Some(value.to_string()), _ => None, }; let settings = HttpSettings::default().with_environment(&env); assert_eq!(settings.http2, expected); assert_eq!(settings.httpx_transport, expected); assert_eq!(settings.trust_proxy_env, expected); + assert_eq!(settings.ignore_proxy_env, expected); } } diff --git a/litellm-rust/crates/llms/src/custom_httpx/transport.rs b/litellm-rust/crates/llms/src/custom_httpx/transport.rs index 172dd96476a..c42cdf410f6 100644 --- a/litellm-rust/crates/llms/src/custom_httpx/transport.rs +++ b/litellm-rust/crates/llms/src/custom_httpx/transport.rs @@ -11,7 +11,7 @@ pub enum Error { impl Error { pub fn from_reqwest_before_dispatch(error: reqwest::Error) -> Self { let before_dispatch = !error.is_timeout() && (error.is_connect() || error.is_builder()); - let message = error.without_url().to_string(); + let message = describe(error); if before_dispatch { Self::Connect(message) } else { @@ -22,10 +22,18 @@ impl Error { impl From for Error { fn from(error: reqwest::Error) -> Self { - Self::Network(error.without_url().to_string()) + Self::Network(describe(error)) } } +fn describe(error: reqwest::Error) -> String { + let error = error.without_url(); + std::iter::successors(std::error::Error::source(&error), |cause| cause.source()) + .fold(error.to_string(), |message, cause| { + format!("{message}: {cause}") + }) +} + #[cfg(test)] mod tests { #[tokio::test] @@ -47,6 +55,32 @@ mod tests { assert!(!error.to_string().contains("private")); } + fn root_cause(error: &dyn std::error::Error) -> Option { + match error.source() { + Some(cause) => root_cause(cause).or_else(|| Some(cause.to_string())), + None => None, + } + } + + #[tokio::test] + async fn network_error_message_names_the_underlying_cause() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); + let address = listener.local_addr().expect("address"); + drop(listener); + let error = reqwest::Client::builder() + .no_proxy() + .build() + .expect("client") + .get(format!("http://{address}/private?api_key=secret")) + .send() + .await + .expect_err("nothing listens on the port"); + let root_cause = root_cause(&error).expect("reqwest reports a cause"); + let message = crate::custom_httpx::transport::Error::from(error).to_string(); + assert!(message.contains(&root_cause), "{message}"); + assert!(!message.contains("secret")); + } + #[tokio::test] async fn request_timeout_is_not_safe_to_retry_as_a_connect_failure() { use std::time::Duration; diff --git a/litellm-rust/crates/python-bridge/python_settings.json b/litellm-rust/crates/python-bridge/python_settings.json index 64dd01a0a84..40e36a900d3 100644 --- a/litellm-rust/crates/python-bridge/python_settings.json +++ b/litellm-rust/crates/python-bridge/python_settings.json @@ -7,6 +7,7 @@ "force_ipv4", "http2", "aiohttp_trust_env", + "disable_aiohttp_trust_env", "disable_aiohttp_transport", "user_agent" ] diff --git a/litellm-rust/crates/python-bridge/src/http.rs b/litellm-rust/crates/python-bridge/src/http.rs index cf7ca05515e..2ab6517b61d 100644 --- a/litellm-rust/crates/python-bridge/src/http.rs +++ b/litellm-rust/crates/python-bridge/src/http.rs @@ -72,6 +72,7 @@ struct PythonHttpSettings<'py> { force_ipv4: bool, http2: bool, aiohttp_trust_env: bool, + disable_aiohttp_trust_env: bool, disable_aiohttp_transport: bool, user_agent: String, } @@ -92,6 +93,7 @@ fn settings(value: &Bound<'_, PyAny>) -> PyResult { httpx_transport: python.disable_aiohttp_transport, user_agent: Some(python.user_agent), trust_proxy_env: python.aiohttp_trust_env, + ignore_proxy_env: python.disable_aiohttp_trust_env, ..HttpSettings::default() }) } @@ -133,6 +135,7 @@ defaults = dict( force_ipv4=False, http2=False, aiohttp_trust_env=False, + disable_aiohttp_trust_env=False, disable_aiohttp_transport=False, user_agent='litellm/test', ) @@ -177,6 +180,7 @@ ssl_ecdh_curve='X25519', force_ipv4=True, http2=True, aiohttp_trust_env=True, +disable_aiohttp_trust_env=True, disable_aiohttp_transport=True, user_agent='litellm/9.9.9', ", @@ -194,6 +198,7 @@ user_agent='litellm/9.9.9', httpx_transport: true, user_agent: Some("litellm/9.9.9".into()), trust_proxy_env: true, + ignore_proxy_env: true, ..HttpSettings::default() } ); @@ -295,11 +300,15 @@ user_agent='litellm/9.9.9', #[rstest] #[case::asynchronous(true, false)] #[case::synchronous(false, true)] - fn synchronous_calls_honor_environment_proxies_like_httpx( + fn synchronous_calls_honor_environment_proxies_even_when_aiohttp_opts_out( #[case] asynchronous: bool, #[case] expected: bool, ) { - let settings = for_call(HttpSettings::default(), None, asynchronous); + let opted_out = HttpSettings { + ignore_proxy_env: true, + ..HttpSettings::default() + }; + let settings = for_call(opted_out, None, asynchronous); let config = HttpClientConfig::resolve(&settings).unwrap(); assert_eq!(config.trust_proxy_env, expected); } diff --git a/litellm/rust_bridge/settings.py b/litellm/rust_bridge/settings.py index ad478fb28b5..491312c97b6 100644 --- a/litellm/rust_bridge/settings.py +++ b/litellm/rust_bridge/settings.py @@ -12,6 +12,7 @@ class HttpSettings: force_ipv4: bool http2: bool aiohttp_trust_env: bool + disable_aiohttp_trust_env: bool disable_aiohttp_transport: bool user_agent: str @@ -28,6 +29,7 @@ def http_settings() -> HttpSettings: force_ipv4=litellm.force_ipv4, http2=litellm.http2, aiohttp_trust_env=litellm.aiohttp_trust_env, + disable_aiohttp_trust_env=litellm.disable_aiohttp_trust_env, disable_aiohttp_transport=litellm.disable_aiohttp_transport, user_agent=default_user_agent(), ) diff --git a/tests/test_litellm/rust_bridge/test_settings.py b/tests/test_litellm/rust_bridge/test_settings.py index dce2324de08..f4f9cbc8eec 100644 --- a/tests/test_litellm/rust_bridge/test_settings.py +++ b/tests/test_litellm/rust_bridge/test_settings.py @@ -26,6 +26,7 @@ def test_http_settings_reads_the_litellm_globals(monkeypatch: pytest.MonkeyPatch monkeypatch.setattr(litellm, "force_ipv4", True) monkeypatch.setattr(litellm, "http2", True) monkeypatch.setattr(litellm, "aiohttp_trust_env", True) + monkeypatch.setattr(litellm, "disable_aiohttp_trust_env", True) monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) assert settings.http_settings() == settings.HttpSettings( @@ -36,6 +37,7 @@ def test_http_settings_reads_the_litellm_globals(monkeypatch: pytest.MonkeyPatch force_ipv4=True, http2=True, aiohttp_trust_env=True, + disable_aiohttp_trust_env=True, disable_aiohttp_transport=True, user_agent=default_user_agent(), )