Track retry counts originating from early middleware errors (#17274)

## Summary

Fixes #17266.

The retry count was getting dropped by
`ErrorKind::from_retry_middleware` and `<Error as
From<ErrorKind>>::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.
This commit is contained in:
Tomasz Kramkowski
2026-01-05 20:06:38 +00:00
committed by GitHub
parent 6f9fced6c6
commit df62ee6f4d
2 changed files with 57 additions and 4 deletions

View File

@@ -246,9 +246,15 @@ impl Error {
impl From<ErrorKind> 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::<OfflineError>() {
return Self::Offline(err.url().to_string());
}
if let Some(reqwest_retry::RetryError::WithRetries { retries, .. }) =
underlying.downcast_ref::<reqwest_retry::RetryError>()
{
let retries = *retries;
return Self::RequestWithRetries {
source: Box::new(Self::WrappedReqwestError(
url,
WrappedReqwestError::from(err),
)),
retries,
};
}
}
Self::WrappedReqwestError(url, WrappedReqwestError::from(err))

View File

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