diff --git a/crates/uv-bin-install/src/lib.rs b/crates/uv-bin-install/src/lib.rs index 7813837e8..ba5965a99 100644 --- a/crates/uv-bin-install/src/lib.rs +++ b/crates/uv-bin-install/src/lib.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use std::pin::Pin; use std::str::FromStr; use std::task::{Context, Poll}; +use std::time::{Duration, SystemTimeError}; use futures::{StreamExt, TryStreamExt}; use reqwest_retry::policies::ExponentialBackoff; @@ -248,11 +249,16 @@ pub enum Error { #[error("Failed to detect platform")] Platform(#[from] uv_platform::Error), - #[error("Request failed after {retries} {subject}", subject = if *retries > 1 { "retries" } else { "retry" })] + #[error( + "Request failed after {retries} {subject} in {duration:.1}s", + subject = if *retries > 1 { "retries" } else { "retry" }, + duration = duration.as_secs_f32() + )] RetriedError { #[source] err: Box, retries: u32, + duration: Duration, }, #[error("Failed to fetch version manifest from: {url}")] @@ -287,6 +293,9 @@ pub enum Error { #[error("Unsupported archive format: {0}")] UnsupportedArchiveFormat(String), + + #[error(transparent)] + SystemTime(#[from] SystemTimeError), } impl Error { @@ -352,6 +361,7 @@ pub async fn find_matching_version( Err(Error::RetriedError { err: Box::new(err), retries: retry_state.total_retries(), + duration: retry_state.duration()?, }) } else { Err(err) @@ -628,6 +638,7 @@ async fn download_and_unpack_with_retry( Err(Error::RetriedError { err: Box::new(err), retries: retry_state.total_retries(), + duration: retry_state.duration()?, }) } else { Err(err) @@ -678,6 +689,8 @@ async fn download_and_unpack( return Err(Error::RetriedError { err: Box::new(err), retries, + // This value is overwritten in `download_and_unpack_with_retry`. + duration: Duration::default(), }); } return Err(err); diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 51694d670..399525cbd 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -4,7 +4,7 @@ use std::fmt::Write; use std::num::ParseIntError; use std::path::Path; use std::sync::Arc; -use std::time::{Duration, SystemTime}; +use std::time::{Duration, SystemTime, SystemTimeError}; use std::{env, io, iter}; use anyhow::anyhow; @@ -1317,6 +1317,11 @@ impl RetryState { self.total_retries } + /// The total duration from the first request to the (failure) of the last request. + pub fn duration(&self) -> Result { + self.start_time.elapsed() + } + /// Determines whether request should be retried. /// /// Takes the number of retries from nested layers associated with the specific `err` type as diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index d6eddcd95..2b8e0b78a 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -1,3 +1,4 @@ +use std::time::{Duration, Instant}; use std::{borrow::Cow, path::Path}; use futures::FutureExt; @@ -105,7 +106,11 @@ pub enum CachedClientError { Client(Error), /// Track retries before a callback explicitly, as we can't attach them to the callback error /// type. - Callback { retries: u32, err: CallbackError }, + Callback { + retries: u32, + err: CallbackError, + duration: Duration, + }, } impl CachedClientError { @@ -113,7 +118,15 @@ impl CachedClientError Self { match self { Self::Client(err) => Self::Client(err.with_retries(retries)), - Self::Callback { retries: _, err } => Self::Callback { retries, err }, + Self::Callback { + retries: _, + err, + duration, + } => Self::Callback { + retries, + err, + duration, + }, } } @@ -151,9 +164,11 @@ impl + std::error::Error + 'static> From> for fn from(error: CachedClientError) -> Self { match error { CachedClientError::Client(error) => error, - CachedClientError::Callback { retries, err } => { - Self::new(err.into().into_kind(), retries) - } + CachedClientError::Callback { + retries, + err, + duration, + } => Self::new(err.into().into_kind(), retries, duration), } } } @@ -261,6 +276,7 @@ impl CachedClient { response_callback: Callback, ) -> Result> { let fresh_req = req.try_clone().expect("HTTP request must be cloneable"); + let start = Instant::now(); let cached_response = if let Some(cached) = Self::read_cache(cache_entry).await { self.send_cached(req, cache_control, cached) .boxed_local() @@ -339,6 +355,7 @@ impl CachedClient { self.run_response_callback( cache_entry, cache_policy, + start, response, response_callback, ) @@ -360,10 +377,11 @@ impl CachedClient { cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { + let start = Instant::now(); let (response, cache_policy) = self.fresh_request(req, cache_control).await?; let payload = self - .run_response_callback(cache_entry, cache_policy, response, async |resp| { + .run_response_callback(cache_entry, cache_policy, start, response, async |resp| { let payload = response_callback(resp).await?; Ok(SerdeCacheable { inner: payload }) }) @@ -384,9 +402,16 @@ impl CachedClient { response_callback: Callback, ) -> Result> { let _ = fs_err::tokio::remove_file(&cache_entry.path()).await; + let start = Instant::now(); let (response, cache_policy) = self.fresh_request(req, cache_control).await?; - self.run_response_callback(cache_entry, cache_policy, response, response_callback) - .await + self.run_response_callback( + cache_entry, + cache_policy, + start, + response, + response_callback, + ) + .await } async fn run_response_callback< @@ -397,6 +422,7 @@ impl CachedClient { &self, cache_entry: &CacheEntry, cache_policy: Option>, + start: Instant, response: Response, response_callback: Callback, ) -> Result> { @@ -410,7 +436,11 @@ impl CachedClient { let data = response_callback(response) .boxed_local() .await - .map_err(|err| CachedClientError::Callback { retries, err })?; + .map_err(|err| CachedClientError::Callback { + retries, + err, + duration: start.elapsed(), + })?; let Some(cache_policy) = cache_policy else { return Ok(data.into_target()); }; @@ -516,12 +546,13 @@ impl CachedClient { ) -> Result { let url = DisplaySafeUrl::from_url(req.url().clone()); debug!("Sending revalidation request for: {url}"); + let start = Instant::now(); let mut response = self .0 .execute(req) .instrument(info_span!("revalidation_request", url = url.as_str())) .await - .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; + .map_err(|err| Error::from_reqwest_middleware(url.clone(), err, start))?; trace!( "Received response for revalidation request with status {} for: {}", response.status(), @@ -581,11 +612,12 @@ impl CachedClient { let url = DisplaySafeUrl::from_url(req.url().clone()); debug!("Sending fresh {} request for: {}", req.method(), url); let cache_policy_builder = CachePolicyBuilder::new(&req); + let start = Instant::now(); let mut response = self .0 .execute(req) .await - .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; + .map_err(|err| Error::from_reqwest_middleware(url.clone(), err, start))?; trace!( "Received response for fresh request with status {} for: {}", response.status(), @@ -611,6 +643,7 @@ impl CachedClient { return Err(Error::new( ErrorKind::from_reqwest_with_problem_details(url, status_error, problem_details), retry_count.unwrap_or_default(), + start.elapsed(), )); } diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index dc41a8366..5e7b39af6 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -1,10 +1,12 @@ +use std::fmt::{Display, Formatter}; +use std::ops::Deref; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + use async_http_range_reader::AsyncHttpRangeReaderError; use async_zip::error::ZipError; use reqwest::Response; use serde::Deserialize; -use std::fmt::{Display, Formatter}; -use std::ops::Deref; -use std::path::PathBuf; use tracing::warn; use uv_cache::Error as CacheError; @@ -114,6 +116,7 @@ impl ProblemDetails { pub struct Error { kind: Box, retries: u32, + duration: Duration, } impl Display for Error { @@ -121,9 +124,10 @@ impl Display for Error { if self.retries > 0 { write!( f, - "Request failed after {retries} {subject}", + "Request failed after {retries} {subject} in {duration:.1}s", retries = self.retries, - subject = if self.retries > 1 { "retries" } else { "retry" } + subject = if self.retries > 1 { "retries" } else { "retry" }, + duration = self.duration.as_secs_f32(), ) } else { Display::fmt(&self.kind, f) @@ -143,10 +147,11 @@ impl std::error::Error for Error { impl Error { /// Create a new [`Error`] with the given [`ErrorKind`] and number of retries. - pub fn new(kind: ErrorKind, retries: u32) -> Self { + pub fn new(kind: ErrorKind, retries: u32, duration: Duration) -> Self { Self { kind: Box::new(kind), retries, + duration, } } @@ -155,6 +160,12 @@ impl Error { self.retries } + /// Return the time taken for network requests, including retries, backoff and jitter, + /// before this error was returned. + pub fn duration(&self) -> Duration { + self.duration + } + /// Convert this error into an [`ErrorKind`]. pub fn into_kind(self) -> ErrorKind { *self.kind @@ -189,6 +200,7 @@ impl Error { pub(crate) fn from_reqwest_middleware( url: DisplaySafeUrl, err: reqwest_middleware::Error, + start: Instant, ) -> Self { if let reqwest_middleware::Error::Middleware(ref underlying) = err { if let Some(offline_err) = underlying.downcast_ref::() { @@ -201,6 +213,7 @@ impl Error { return Self::new( ErrorKind::WrappedReqwestError(url, WrappedReqwestError::from(err)), retries, + start.elapsed(), ); } } @@ -316,6 +329,7 @@ impl From for Error { Self { kind: Box::new(kind), retries: 0, + duration: Duration::default(), } } } diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 9123c4d35..f20354e3d 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use std::str::FromStr; use std::task::{Context, Poll}; +use std::time::{Duration, Instant, SystemTimeError}; use std::{env, io}; use futures::TryStreamExt; @@ -54,11 +55,16 @@ pub enum Error { TooManyParts(String), #[error("Failed to download {0}")] NetworkError(DisplaySafeUrl, #[source] WrappedReqwestError), - #[error("Request failed after {retries} {subject}", subject = if *retries > 1 { "retries" } else { "retry" })] + #[error( + "Request failed after {retries} {subject} in {duration:.1}s", + subject = if *retries > 1 { "retries" } else { "retry" }, + duration = duration.as_secs_f32() + )] NetworkErrorWithRetries { #[source] err: Box, retries: u32, + duration: Duration, }, #[error("Failed to download {0}")] NetworkMiddlewareError(DisplaySafeUrl, #[source] anyhow::Error), @@ -116,6 +122,8 @@ pub enum Error { BuildVersion(#[from] BuildVersionError), #[error("No download URL found for Python")] NoPythonDownloadUrlFound, + #[error(transparent)] + SystemTime(#[from] SystemTimeError), } impl Error { @@ -1183,6 +1191,7 @@ impl ManagedPythonDownload { Err(Error::NetworkErrorWithRetries { err: Box::new(err), retries: retry_state.total_retries(), + duration: retry_state.duration()?, }) } else { Err(err) @@ -1698,12 +1707,14 @@ impl Error { url: DisplaySafeUrl, err: reqwest::Error, retries: Option, + start: Instant, ) -> Self { let err = Self::NetworkError(url, WrappedReqwestError::from(err)); if let Some(retries) = retries { Self::NetworkErrorWithRetries { err: Box::new(err), retries, + duration: start.elapsed(), } } else { err @@ -1815,6 +1826,7 @@ async fn read_url( Ok((Either::Left(reader), Some(size))) } else { + let start = Instant::now(); let response = client .for_host(url) .get(Url::from(url.clone())) @@ -1830,7 +1842,7 @@ async fn read_url( // Check the status code. let response = response .error_for_status() - .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count))?; + .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count, start))?; let size = response.content_length(); let stream = response diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index c2a0e0abd..572274931 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -248,6 +248,7 @@ impl ResolverProvider for DefaultResolverProvider<'_, Con Err(err) => match err { uv_distribution::Error::Client(client) => { let retries = client.retries(); + let duration = client.duration(); match client.into_kind() { uv_client::ErrorKind::Offline(_) => { Ok(MetadataResponse::Unavailable(MetadataUnavailable::Offline)) @@ -262,7 +263,7 @@ impl ResolverProvider for DefaultResolverProvider<'_, Con MetadataUnavailable::InvalidStructure(Arc::new(err)), )) } - kind => Err(uv_client::Error::new(kind, retries).into()), + kind => Err(uv_client::Error::new(kind, retries, duration).into()), } } uv_distribution::Error::WheelMetadataVersionMismatch { .. } => { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index bfd714d13..74652b7d4 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -13321,13 +13321,14 @@ async fn add_unexpected_error_code() -> Result<()> { })?; uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--index").arg(server.uri()) - .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @" + .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true") + .env(EnvVars::UV_HTTP_RETRIES, "1"), @" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Request failed after 3 retries + error: Request failed after 1 retry in [TIME] Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/` Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/) " diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs index 3c53af37b..49575052a 100644 --- a/crates/uv/tests/it/network.rs +++ b/crates/uv/tests/it/network.rs @@ -240,8 +240,7 @@ async fn simple_http_500() { let (_server_drop_guard, mock_server_uri) = http_error_server().await; - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg("tqdm") .arg("--index-url") @@ -252,9 +251,9 @@ async fn simple_http_500() { ----- stdout ----- ----- stderr ----- - error: Request failed after 3 retries - Caused by: Failed to fetch: `[SERVER]/tqdm/` - Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/tqdm/) + error: Request failed after 3 retries in [TIME] + Caused by: Failed to fetch: `http://[LOCALHOST]/tqdm/` + Caused by: HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/tqdm/) "); } @@ -265,8 +264,7 @@ async fn simple_io_err() { let (_server_drop_guard, mock_server_uri) = io_error_server().await; - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg("tqdm") .arg("--index-url") @@ -277,9 +275,9 @@ async fn simple_io_err() { ----- stdout ----- ----- stderr ----- - error: Request failed after 3 retries - Caused by: Failed to fetch: `[SERVER]/tqdm/` - Caused by: error sending request for url ([SERVER]/tqdm/) + error: Request failed after 3 retries in [TIME] + Caused by: Failed to fetch: `http://[LOCALHOST]/tqdm/` + Caused by: error sending request for url (http://[LOCALHOST]/tqdm/) Caused by: client error (SendRequest) Caused by: connection closed before message completed "); @@ -292,8 +290,7 @@ async fn find_links_http_500() { let (_server_drop_guard, mock_server_uri) = http_error_server().await; - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg("tqdm") .arg("--no-index") @@ -305,10 +302,10 @@ async fn find_links_http_500() { ----- stdout ----- ----- stderr ----- - error: Failed to read `--find-links` URL: [SERVER]/ - Caused by: Request failed after 3 retries - Caused by: Failed to fetch: `[SERVER]/` - Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/) + error: Failed to read `--find-links` URL: http://[LOCALHOST]/ + Caused by: Request failed after 3 retries in [TIME] + Caused by: Failed to fetch: `http://[LOCALHOST]/` + Caused by: HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/) "); } @@ -319,8 +316,7 @@ async fn find_links_io_error() { let (_server_drop_guard, mock_server_uri) = io_error_server().await; - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg("tqdm") .arg("--no-index") @@ -332,10 +328,10 @@ async fn find_links_io_error() { ----- stdout ----- ----- stderr ----- - error: Failed to read `--find-links` URL: [SERVER]/ - Caused by: Request failed after 3 retries - Caused by: Failed to fetch: `[SERVER]/` - Caused by: error sending request for url ([SERVER]/) + error: Failed to read `--find-links` URL: http://[LOCALHOST]/ + Caused by: Request failed after 3 retries in [TIME] + Caused by: Failed to fetch: `http://[LOCALHOST]/` + Caused by: error sending request for url (http://[LOCALHOST]/) Caused by: client error (SendRequest) Caused by: connection closed before message completed "); @@ -349,8 +345,7 @@ async fn find_links_mixed_error() { let (_server_drop_guard, mock_server_uri) = mixed_error_server().await; - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg("tqdm") .arg("--no-index") @@ -362,10 +357,10 @@ async fn find_links_mixed_error() { ----- stdout ----- ----- stderr ----- - error: Failed to read `--find-links` URL: [SERVER]/ - Caused by: Request failed after 3 retries - Caused by: Failed to fetch: `[SERVER]/` - Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/) + error: Failed to read `--find-links` URL: http://[LOCALHOST]/ + Caused by: Request failed after 3 retries in [TIME] + Caused by: Failed to fetch: `http://[LOCALHOST]/` + Caused by: HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/) "); } @@ -380,8 +375,7 @@ async fn direct_url_http_500() { let tqdm_url = format!( "{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" ); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg(format!("tqdm @ {tqdm_url}")) .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @" @@ -390,10 +384,10 @@ async fn direct_url_http_500() { ----- stdout ----- ----- stderr ----- - × Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ├─▶ Request failed after 3 retries - ├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ╰─▶ HTTP status server error (500 Internal Server Error) for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) + × Failed to download `tqdm @ http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Request failed after 3 retries in [TIME] + ├─▶ Failed to fetch: `http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ╰─▶ HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) "); } @@ -407,8 +401,7 @@ async fn direct_url_io_error() { let tqdm_url = format!( "{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" ); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg(format!("tqdm @ {tqdm_url}")) .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @" @@ -417,10 +410,10 @@ async fn direct_url_io_error() { ----- stdout ----- ----- stderr ----- - × Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ├─▶ Request failed after 3 retries - ├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ├─▶ error sending request for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) + × Failed to download `tqdm @ http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Request failed after 3 retries in [TIME] + ├─▶ Failed to fetch: `http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ error sending request for url (http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) ├─▶ client error (SendRequest) ╰─▶ connection closed before message completed "); @@ -437,8 +430,7 @@ async fn direct_url_mixed_error() { let tqdm_url = format!( "{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl" ); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg(format!("tqdm @ {tqdm_url}")) .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @" @@ -447,10 +439,10 @@ async fn direct_url_mixed_error() { ----- stdout ----- ----- stderr ----- - × Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ├─▶ Request failed after 3 retries - ├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` - ╰─▶ HTTP status server error (500 Internal Server Error) for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) + × Failed to download `tqdm @ http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Request failed after 3 retries in [TIME] + ├─▶ Failed to fetch: `http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl` + ╰─▶ HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl) "); } @@ -493,8 +485,7 @@ async fn python_install_http_500() { let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .python_install() .arg("cpython-3.10.0-darwin-aarch64-none") .arg("--python-downloads-json-url") @@ -505,10 +496,10 @@ async fn python_install_http_500() { ----- stdout ----- ----- stderr ----- - error: Failed to install cpython-3.10.0-macos-aarch64-none - Caused by: Request failed after 3 retries - Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst - Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst) + error: Failed to install cpython-3.10.0-[PLATFORM] + Caused by: Request failed after 3 retries in [TIME] + Caused by: Failed to download http://[LOCALHOST]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-[PLATFORM]-pgo%2Blto-20211017T1616.tar.zst + Caused by: HTTP status server error (500 Internal Server Error) for url (http://[LOCALHOST]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-[PLATFORM]-pgo%2Blto-20211017T1616.tar.zst) "); } @@ -524,8 +515,7 @@ async fn python_install_io_error() { let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .python_install() .arg("cpython-3.10.0-darwin-aarch64-none") .arg("--python-downloads-json-url") @@ -536,10 +526,10 @@ async fn python_install_io_error() { ----- stdout ----- ----- stderr ----- - error: Failed to install cpython-3.10.0-macos-aarch64-none - Caused by: Request failed after 3 retries - Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst - Caused by: error sending request for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst) + error: Failed to install cpython-3.10.0-[PLATFORM] + Caused by: Request failed after 3 retries in [TIME] + Caused by: Failed to download http://[LOCALHOST]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-[PLATFORM]-pgo%2Blto-20211017T1616.tar.zst + Caused by: error sending request for url (http://[LOCALHOST]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-[PLATFORM]-pgo%2Blto-20211017T1616.tar.zst) Caused by: client error (SendRequest) Caused by: connection closed before message completed "); @@ -611,7 +601,7 @@ async fn install_http_retries() { ----- stdout ----- ----- stderr ----- - error: Request failed after 5 retries + error: Request failed after 5 retries in [TIME] Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/` Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/) " @@ -643,7 +633,7 @@ async fn install_http_retry_low_level() { ----- stdout ----- ----- stderr ----- - error: Request failed after 1 retry + error: Request failed after 1 retry in [TIME] Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/` Caused by: error sending request for url (http://[LOCALHOST]/anyio/) Caused by: client error (SendRequest) @@ -684,8 +674,7 @@ async fn rfc9457_problem_details_license_violation() { let mock_server_uri = server.uri(); let tqdm_url = format!("{mock_server_uri}/packages/tqdm-4.67.1-py3-none-any.whl"); - let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; - uv_snapshot!(filters, context + uv_snapshot!(context.filters(), context .pip_install() .arg(format!("tqdm @ {tqdm_url}")), @" success: false @@ -693,10 +682,10 @@ async fn rfc9457_problem_details_license_violation() { ----- stdout ----- ----- stderr ----- - × Failed to download `tqdm @ [SERVER]/packages/tqdm-4.67.1-py3-none-any.whl` - ├─▶ Failed to fetch: `[SERVER]/packages/tqdm-4.67.1-py3-none-any.whl` + × Failed to download `tqdm @ http://[LOCALHOST]/packages/tqdm-4.67.1-py3-none-any.whl` + ├─▶ Failed to fetch: `http://[LOCALHOST]/packages/tqdm-4.67.1-py3-none-any.whl` ├─▶ Server message: License Compliance Issue, This package version has a license that violates organizational policy. - ╰─▶ HTTP status client error (403 Forbidden) for url ([SERVER]/packages/tqdm-4.67.1-py3-none-any.whl) + ╰─▶ HTTP status client error (403 Forbidden) for url (http://[LOCALHOST]/packages/tqdm-4.67.1-py3-none-any.whl) "); } @@ -1079,7 +1068,7 @@ async fn retry_read_timeout_index() { ----- stdout ----- ----- stderr ----- - error: Request failed after 1 retry + error: Request failed after 1 retry in [TIME] Caused by: Failed to fetch: `http://[LOCALHOST]/tqdm/` Caused by: error decoding response body Caused by: request or response body error @@ -1105,7 +1094,7 @@ async fn retry_read_timeout_stream() { ----- stderr ----- × Failed to download `tqdm @ http://[LOCALHOST]/tqdm-0.1-py3-none-any.whl` - ├─▶ Request failed after 1 retry + ├─▶ Request failed after 1 retry in [TIME] ├─▶ Failed to read metadata: `http://[LOCALHOST]/tqdm-0.1-py3-none-any.whl` ├─▶ Failed to read from zip file ├─▶ an upstream reader returned an error: Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: [TIME]). diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 547861903..a5c0d0e4f 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -14326,7 +14326,7 @@ fn warn_on_lzma_wheel() { ----- stderr ----- × Failed to download `futzed-lzma @ https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl` - ├─▶ Request failed after 3 retries + ├─▶ Request failed after 3 retries in [TIME] ├─▶ Failed to read metadata: `https://github.com/astral-sh/futzed-wheels/releases/download/v2026.02.09.2/futzed_lzma-0.1.0-py3-none-any.whl` ├─▶ Failed to read from zip file ├─▶ an upstream reader returned an error: stream/file format not recognized