From df62ee6f4d2737c16e5ccf5dc205ac894058f477 Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Mon, 5 Jan 2026 20:06:38 +0000 Subject: [PATCH] Track retry counts originating from early middleware errors (#17274) ## Summary Fixes #17266. The retry count was getting dropped by `ErrorKind::from_retry_middleware` and `>::from` so we were doing more retries than we should have. ## Test Plan Added a testcase for the specific error path in the issue. Added an expect to the other retry test too. --- crates/uv-client/src/error.rs | 24 ++++++++++++++++++++--- crates/uv/tests/it/network.rs | 37 ++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index d82773386..beee98501 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -246,9 +246,15 @@ impl Error { impl From for Error { fn from(kind: ErrorKind) -> Self { - Self { - kind: Box::new(kind), - retries: 0, + match kind { + ErrorKind::RequestWithRetries { source, retries } => Self { + kind: source, + retries, + }, + other => Self { + kind: Box::new(other), + retries: 0, + }, } } } @@ -391,6 +397,18 @@ impl ErrorKind { if let Some(err) = underlying.downcast_ref::() { return Self::Offline(err.url().to_string()); } + if let Some(reqwest_retry::RetryError::WithRetries { retries, .. }) = + underlying.downcast_ref::() + { + let retries = *retries; + return Self::RequestWithRetries { + source: Box::new(Self::WrappedReqwestError( + url, + WrappedReqwestError::from(err), + )), + retries, + }; + } } Self::WrappedReqwestError(url, WrappedReqwestError::from(err)) diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs index cad808e3e..2b62a7cad 100644 --- a/crates/uv/tests/it/network.rs +++ b/crates/uv/tests/it/network.rs @@ -5,7 +5,7 @@ use http::StatusCode; use serde_json::json; use uv_static::EnvVars; use wiremock::matchers::method; -use wiremock::{Mock, MockServer, ResponseTemplate}; +use wiremock::{Mock, MockServer, Request, ResponseTemplate}; use crate::common::{TestContext, uv_snapshot}; @@ -392,6 +392,7 @@ async fn install_http_retries() { // Create a server that always fails, so we can see the number of retries used Mock::given(method("GET")) .respond_with(ResponseTemplate::new(503)) + .expect(6) .mount(&server) .await; @@ -455,6 +456,40 @@ async fn install_http_retries() { ); } +#[tokio::test] +async fn install_http_retry_low_level() { + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + + // Create a server that fails with a more fundamental error so we trigger + // earlier error paths + Mock::given(method("GET")) + .respond_with_err(|_: &'_ Request| io::Error::new(io::ErrorKind::ConnectionReset, "error")) + .expect(2) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("anyio") + .arg("--index") + .arg(server.uri()) + .env(EnvVars::UV_HTTP_RETRIES, "1") + .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Request failed after 1 retry + Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/` + Caused by: error sending request for url (http://[LOCALHOST]/anyio/) + Caused by: client error (SendRequest) + Caused by: connection closed before message completed + " + ); +} + /// Test problem details with a 403 error containing license compliance information #[tokio::test] async fn rfc9457_problem_details_license_violation() {