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 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-23 11:37:10 -07:00
parent ca8c6c698f
commit 326e5cc4a8
6 changed files with 71 additions and 1 deletions

View file

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

View file

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

View file

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

View file

@ -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::<Responses>::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)]

View file

@ -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<dyn SecretSource>,
) -> litellm_inference_responses::ResponsesRoute {

View file

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