From 326e5cc4a841d37d859523c10656b979dc65903b Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 23 Oct 2026 11:37:10 -0700 Subject: [PATCH] test(inference): pin provider URL redaction on every route's transport error Chat, Messages, Responses and transcription each get a route-level test that fails if a reqwest error reaches the caller with its URL. The Messages stream decoder also takes body errors through litellm_http::Error like every other reqwest error Co-Authored-By: Claude Opus 5.5 --- .../inference-chat/tests/chat_completions.rs | 17 +++++++++++++++++ .../crates/inference-messages/src/handler.rs | 2 +- .../tests/messages/response.rs | 14 ++++++++++++++ .../inference-responses/tests/responses.rs | 19 +++++++++++++++++++ .../inference-responses/tests/support/mod.rs | 3 +++ .../tests/audio_transcription.rs | 17 +++++++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/litellm-rust/crates/inference-chat/tests/chat_completions.rs b/litellm-rust/crates/inference-chat/tests/chat_completions.rs index ddcf2c0f5c9..336b5f1846d 100644 --- a/litellm-rust/crates/inference-chat/tests/chat_completions.rs +++ b/litellm-rust/crates/inference-chat/tests/chat_completions.rs @@ -221,6 +221,23 @@ async fn a_connection_that_is_never_established_returns_a_connect_error( ); } +#[rstest] +#[tokio::test] +async fn a_transport_error_does_not_echo_the_provider_url( + request: ChatCompletionsRequest<'static>, +) { + let base = format!("{UNREACHABLE_BASE}/tenant-token-1234"); + + let error = complete(ChatCompletionsRequest { + api_base: Some(&base), + ..request + }) + .await + .expect_err("nothing is listening"); + + assert!(!error.to_string().contains("tenant-token-1234"), "{error}"); +} + #[rstest] #[tokio::test] async fn a_timeout_after_sending_returns_a_network_error(request: ChatCompletionsRequest<'static>) { diff --git a/litellm-rust/crates/inference-messages/src/handler.rs b/litellm-rust/crates/inference-messages/src/handler.rs index 979791f949e..5a067db937d 100644 --- a/litellm-rust/crates/inference-messages/src/handler.rs +++ b/litellm-rust/crates/inference-messages/src/handler.rs @@ -359,7 +359,7 @@ fn decoded_chunks( let bytes: ByteStream = response .bytes_stream() .inspect_ok(move |chunk| log_chunk(provider, "provider_response", chunk)) - .map_err(std::io::Error::other) + .map_err(|error| std::io::Error::other(litellm_http::Error::from(error))) .boxed(); futures_util::stream::try_unfold(decode(bytes), move |mut events| async move { let Some(event) = events.try_next().await? else { diff --git a/litellm-rust/crates/inference-messages/tests/messages/response.rs b/litellm-rust/crates/inference-messages/tests/messages/response.rs index cd87b3c8c4c..0005a90294d 100644 --- a/litellm-rust/crates/inference-messages/tests/messages/response.rs +++ b/litellm-rust/crates/inference-messages/tests/messages/response.rs @@ -255,6 +255,20 @@ async fn a_provider_slower_than_the_timeout_fails_the_call(call: MessagesCall) { assert!(matches!(error, Error::Http(_)), "{error:?}"); } +#[rstest] +#[tokio::test] +async fn a_transport_error_does_not_echo_the_provider_url(call: MessagesCall) { + let error = run(MessagesCall { + api_key: Some("sk".into()), + api_base: Some(format!("{UNREACHABLE_BASE}/tenant-token-1234")), + ..call + }) + .await + .expect_err("nothing is listening"); + + assert!(!error.to_string().contains("tenant-token-1234"), "{error}"); +} + #[rstest] #[tokio::test] async fn the_facade_sends_through_the_injected_http_pool_configuration(call: MessagesCall) { diff --git a/litellm-rust/crates/inference-responses/tests/responses.rs b/litellm-rust/crates/inference-responses/tests/responses.rs index 6d39d55d33c..0d7767fd004 100644 --- a/litellm-rust/crates/inference-responses/tests/responses.rs +++ b/litellm-rust/crates/inference-responses/tests/responses.rs @@ -304,6 +304,25 @@ async fn provider_failures_emit_failure_once( ); } +#[rstest] +#[tokio::test] +async fn a_transport_error_does_not_echo_the_provider_url(call: ResponsesCall) { + let host = RecordingCall::::new(ResponsesCall { + api_base: Some(format!("{UNREACHABLE_BASE}/tenant-token-1234")), + ..call + }); + let call = host.request.lock().unwrap().take().unwrap(); + + let Err(error) = responses_route(no_secrets()) + .execute(call, &host, None) + .await + else { + panic!("nothing is listening"); + }; + + assert!(!error.to_string().contains("tenant-token-1234"), "{error}"); +} + #[rstest] #[case::explicit(true)] #[case::from_secrets(false)] diff --git a/litellm-rust/crates/inference-responses/tests/support/mod.rs b/litellm-rust/crates/inference-responses/tests/support/mod.rs index 81f133a0471..984b878eb21 100644 --- a/litellm-rust/crates/inference-responses/tests/support/mod.rs +++ b/litellm-rust/crates/inference-responses/tests/support/mod.rs @@ -13,6 +13,9 @@ use litellm_secrets::source::SecretSource; use serde_json::Value; use wiremock::{Mock, MockServer, Request, ResponseTemplate, matchers::any}; +/// A port nothing listens on, for calls that must fail before any request is sent. +pub const UNREACHABLE_BASE: &str = "http://127.0.0.1:1"; + pub fn responses_route( secrets: Arc, ) -> litellm_inference_responses::ResponsesRoute { diff --git a/litellm-rust/crates/inference-transcription/tests/audio_transcription.rs b/litellm-rust/crates/inference-transcription/tests/audio_transcription.rs index dd40ac84056..d99b995fc21 100644 --- a/litellm-rust/crates/inference-transcription/tests/audio_transcription.rs +++ b/litellm-rust/crates/inference-transcription/tests/audio_transcription.rs @@ -227,6 +227,23 @@ async fn an_upstream_error_keeps_its_status_and_body( ); } +#[rstest] +#[tokio::test] +async fn a_transport_error_does_not_echo_the_provider_url( + request: AudioTranscriptionRequest<'static>, +) { + let base = format!("{UNREACHABLE_BASE}/tenant-token-1234"); + + let error = transcribe(AudioTranscriptionRequest { + api_base: Some(&base), + ..request + }) + .await + .expect_err("nothing is listening"); + + assert!(!error.to_string().contains("tenant-token-1234"), "{error}"); +} + #[rstest] #[case::not_json(ResponseTemplate::new(200).set_body_string("not json"))] #[case::no_output(json_response(json!({"unexpected": true})))]