diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 01497c9c4..01353f4b5 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use std::env; use std::fmt::Debug; use std::path::Path; use std::str::FromStr; +use std::time::Duration; use async_http_range_reader::AsyncHttpRangeReader; use futures::{FutureExt, TryStreamExt}; @@ -25,7 +25,6 @@ use pypi_types::{Metadata21, SimpleJson}; use uv_auth::safe_copy_url_auth; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_normalize::PackageName; -use uv_warnings::warn_user_once; use crate::cached_client::CacheControl; use crate::html::SimpleHtml; @@ -41,6 +40,7 @@ pub struct RegistryClientBuilder { retries: u32, connectivity: Connectivity, cache: Cache, + timeout: Option, client: Option, } @@ -51,6 +51,7 @@ impl RegistryClientBuilder { cache, connectivity: Connectivity::Online, retries: 3, + timeout: None, client: None, } } @@ -87,30 +88,22 @@ impl RegistryClientBuilder { self } - pub fn build(self) -> RegistryClient { - // Timeout options, matching https://doc.rust-lang.org/nightly/cargo/reference/config.html#httptimeout - // `UV_REQUEST_TIMEOUT` is provided for backwards compatibility with v0.1.6 - let default_timeout = 5 * 60; - let timeout = env::var("UV_HTTP_TIMEOUT") - .or_else(|_| env::var("UV_REQUEST_TIMEOUT")) - .or_else(|_| env::var("HTTP_TIMEOUT")) - .and_then(|value| { - value.parse::() - .or_else(|_| { - // On parse error, warn and use the default timeout - warn_user_once!("Ignoring invalid value from environment for UV_HTTP_TIMEOUT. Expected integer number of seconds, got \"{value}\"."); - Ok(default_timeout) - }) - }) - .unwrap_or(default_timeout); - debug!("Using registry request timeout of {}s", timeout); + #[must_use] + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + pub fn build(self) -> RegistryClient { let client_raw = self.client.unwrap_or_else(|| { // Disallow any connections. - let client_core = ClientBuilder::new() + let mut client_core = ClientBuilder::new() .user_agent("uv") - .pool_max_idle_per_host(20) - .timeout(std::time::Duration::from_secs(timeout)); + .pool_max_idle_per_host(20); + if let Some(timeout) = self.timeout { + debug!("Using registry request timeout of {}s", timeout.as_secs()); + client_core = client_core.timeout(timeout); + } client_core.build().expect("Failed to build HTTP client.") }); @@ -135,7 +128,7 @@ impl RegistryClientBuilder { connectivity: self.connectivity, client_raw, client: CachedClient::new(uncached_client), - timeout, + timeout: self.timeout, } } } @@ -155,7 +148,7 @@ pub struct RegistryClient { /// The connectivity mode to use. connectivity: Connectivity, /// Configured client timeout, in seconds. - timeout: u64, + timeout: Option, } impl RegistryClient { @@ -170,7 +163,7 @@ impl RegistryClient { } /// Return the timeout this client is configured with, in seconds. - pub fn timeout(&self) -> u64 { + pub fn timeout(&self) -> Option { self.timeout } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index b79bb3526..835b7123a 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -76,15 +76,16 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> /// Handle a specific `reqwest` error, and convert it to [`io::Error`]. fn handle_response_errors(&self, err: reqwest::Error) -> io::Error { if err.is_timeout() { - io::Error::new( - io::ErrorKind::TimedOut, - format!( - "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", self.client.timeout() - ), - ) - } else { - io::Error::new(io::ErrorKind::Other, err) + if let Some(timeout) = self.client.timeout() { + return io::Error::new( + io::ErrorKind::TimedOut, + format!( + "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", timeout.as_secs() + ), + ); + } } + io::Error::new(io::ErrorKind::Other, err) } /// Either fetch the wheel or fetch and build the source distribution diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 29c533b02..be96bf8b5 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -3,10 +3,12 @@ //! Integration tests for the resolver. These tests rely on a live network connection, and hit //! `PyPI` directly. +use std::env; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::time::Duration; -use anyhow::Result; +use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; @@ -111,7 +113,34 @@ async fn resolve( markers: &'static MarkerEnvironment, tags: &Tags, ) -> Result { - let client = RegistryClientBuilder::new(Cache::temp()?).build(); + let mut registry_builder = RegistryClientBuilder::new(Cache::temp()?); + + // Timeout options, matching https://doc.rust-lang.org/nightly/cargo/reference/config.html#httptimeout + // `UV_REQUEST_TIMEOUT` is provided for backwards compatibility with v0.1.6 + let timeout_env_vars = ["UV_HTTP_TIMEOUT", "UV_REQUEST_TIMEOUT", "HTTP_TIMEOUT"]; + let timeout_or_error = timeout_env_vars.iter().find_map(|env_key| { + if let Ok(env_value) = env::var(env_key) { + let timeout = env_value.parse::().with_context(|| { + format!( + "Invalid value for {env_key}. \ + Expected integer number of seconds, got \"{env_value}\"." + ) + }); + Some(timeout) + } else { + None + } + }); + + match timeout_or_error { + Some(Ok(timeout)) => { + registry_builder = registry_builder.timeout(Duration::from_secs(timeout)); + } + Some(Err(err)) => return Err(err), + None => {} + } + + let client = registry_builder.build(); let flat_index = FlatIndex::default(); let index = InMemoryIndex::default(); let interpreter = Interpreter::artificial(Platform::current()?, markers.clone());