From a58d0311577e022941d8aac19c6375151efbab2d Mon Sep 17 00:00:00 2001 From: Andrei Berenda Date: Thu, 9 Oct 2025 21:28:30 +0400 Subject: [PATCH] Add `UV_UPLOAD_HTTP_TIMEOUT` and respect `UV_HTTP_TIMEOUT` in uploads (#16040) ## Summary - Move parsing `UV_HTTP_TIMEOUT`, `UV_REQUEST_TIMEOUT` and `HTTP_TIMEOUT` to `EnvironmentOptions` - Add new env varialbe `UV_UPLOAD_HTTP_TIMEOUT` Relates https://github.com/astral-sh/uv/issues/14720 ## Test Plan Tests with existing tests --- crates/uv-client/src/base_client.rs | 26 ++++--------- crates/uv-dev/src/lib.rs | 7 +++- crates/uv-dev/src/validate_zip.rs | 12 +++++- crates/uv-dev/src/wheel_metadata.rs | 12 +++++- crates/uv-settings/src/lib.rs | 50 +++++++++++++++++++++++- crates/uv-static/src/env_vars.rs | 3 ++ crates/uv/src/commands/auth/login.rs | 15 ++++--- crates/uv/src/commands/auth/logout.rs | 16 +++++--- crates/uv/src/commands/auth/token.rs | 16 ++++---- crates/uv/src/commands/publish.rs | 7 ++-- crates/uv/src/lib.rs | 26 ++++++++++--- crates/uv/src/settings.rs | 30 ++++++++++---- crates/uv/tests/it/common/mod.rs | 7 ++++ crates/uv/tests/it/show_settings.rs | 56 +++++++++++++++++++++++++++ crates/uv/tests/it/venv.rs | 18 +++++++++ docs/reference/environment.md | 4 ++ 16 files changed, 243 insertions(+), 62 deletions(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 66b28027c..be50a34de 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -79,7 +79,7 @@ pub struct BaseClientBuilder<'a> { platform: Option<&'a Platform>, auth_integration: AuthIntegration, indexes: Indexes, - default_timeout: Duration, + timeout: Duration, extra_middleware: Option, proxies: Vec, redirect_policy: RedirectPolicy, @@ -137,7 +137,7 @@ impl Default for BaseClientBuilder<'_> { platform: None, auth_integration: AuthIntegration::default(), indexes: Indexes::new(), - default_timeout: Duration::from_secs(30), + timeout: Duration::from_secs(30), extra_middleware: None, proxies: vec![], redirect_policy: RedirectPolicy::default(), @@ -153,12 +153,14 @@ impl BaseClientBuilder<'_> { native_tls: bool, allow_insecure_host: Vec, preview: Preview, + timeout: Duration, ) -> Self { Self { preview, allow_insecure_host, native_tls, connectivity, + timeout, ..Self::default() } } @@ -246,8 +248,8 @@ impl<'a> BaseClientBuilder<'a> { } #[must_use] - pub fn default_timeout(mut self, default_timeout: Duration) -> Self { - self.default_timeout = default_timeout; + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; self } @@ -299,21 +301,7 @@ impl<'a> BaseClientBuilder<'a> { } pub fn build(&self) -> BaseClient { - // 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::var(EnvVars::UV_HTTP_TIMEOUT) - .or_else(|_| env::var(EnvVars::UV_REQUEST_TIMEOUT)) - .or_else(|_| env::var(EnvVars::HTTP_TIMEOUT)) - .and_then(|value| { - value.parse::() - .map(Duration::from_secs) - .or_else(|_| { - // On parse error, warn and use the default timeout - warn_user_once!("Ignoring invalid value from environment for `UV_HTTP_TIMEOUT`. Expected an integer number of seconds, got \"{value}\"."); - Ok(self.default_timeout) - }) - }) - .unwrap_or(self.default_timeout); + let timeout = self.timeout; debug!("Using request timeout of {}s", timeout.as_secs()); // Use the custom client if provided, otherwise create a new one diff --git a/crates/uv-dev/src/lib.rs b/crates/uv-dev/src/lib.rs index f7b7fea53..54e1ff80a 100644 --- a/crates/uv-dev/src/lib.rs +++ b/crates/uv-dev/src/lib.rs @@ -4,6 +4,8 @@ use anyhow::Result; use clap::Parser; use tracing::instrument; +use uv_settings::EnvironmentOptions; + use crate::clear_compile::ClearCompileArgs; use crate::compile::CompileArgs; use crate::generate_all::Args as GenerateAllArgs; @@ -61,9 +63,10 @@ enum Cli { #[instrument] // Anchor span to check for overhead pub async fn run() -> Result<()> { let cli = Cli::parse(); + let environment = EnvironmentOptions::new()?; match cli { - Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args).await?, - Cli::ValidateZip(args) => validate_zip::validate_zip(args).await?, + Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args, environment).await?, + Cli::ValidateZip(args) => validate_zip::validate_zip(args, environment).await?, Cli::Compile(args) => compile::compile(args).await?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, Cli::GenerateAll(args) => generate_all::main(&args).await?, diff --git a/crates/uv-dev/src/validate_zip.rs b/crates/uv-dev/src/validate_zip.rs index 44fe8dbcb..0989f62fa 100644 --- a/crates/uv-dev/src/validate_zip.rs +++ b/crates/uv-dev/src/validate_zip.rs @@ -9,6 +9,7 @@ use uv_cache::{Cache, CacheArgs}; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_pep508::VerbatimUrl; use uv_pypi_types::ParsedUrl; +use uv_settings::EnvironmentOptions; #[derive(Parser)] pub(crate) struct ValidateZipArgs { @@ -17,9 +18,16 @@ pub(crate) struct ValidateZipArgs { cache_args: CacheArgs, } -pub(crate) async fn validate_zip(args: ValidateZipArgs) -> Result<()> { +pub(crate) async fn validate_zip( + args: ValidateZipArgs, + environment: EnvironmentOptions, +) -> Result<()> { let cache = Cache::try_from(args.cache_args)?.init()?; - let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); + let client = RegistryClientBuilder::new( + BaseClientBuilder::default().timeout(environment.http_timeout), + cache, + ) + .build(); let ParsedUrl::Archive(archive) = ParsedUrl::try_from(args.url.to_url())? else { bail!("Only archive URLs are supported"); diff --git a/crates/uv-dev/src/wheel_metadata.rs b/crates/uv-dev/src/wheel_metadata.rs index 411564cab..125a279d3 100644 --- a/crates/uv-dev/src/wheel_metadata.rs +++ b/crates/uv-dev/src/wheel_metadata.rs @@ -10,6 +10,7 @@ use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities, RemoteSource}; use uv_pep508::VerbatimUrl; use uv_pypi_types::ParsedUrl; +use uv_settings::EnvironmentOptions; #[derive(Parser)] pub(crate) struct WheelMetadataArgs { @@ -18,9 +19,16 @@ pub(crate) struct WheelMetadataArgs { cache_args: CacheArgs, } -pub(crate) async fn wheel_metadata(args: WheelMetadataArgs) -> Result<()> { +pub(crate) async fn wheel_metadata( + args: WheelMetadataArgs, + environment: EnvironmentOptions, +) -> Result<()> { let cache = Cache::try_from(args.cache_args)?.init()?; - let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); + let client = RegistryClientBuilder::new( + BaseClientBuilder::default().timeout(environment.http_timeout), + cache, + ) + .build(); let capabilities = IndexCapabilities::default(); let filename = WheelFilename::from_str(&args.url.filename()?)?; diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 72aca3794..3fe41766a 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -1,5 +1,6 @@ use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::time::Duration; use uv_dirs::{system_config_file, user_config_dir}; use uv_flags::EnvironmentFlags; @@ -552,7 +553,8 @@ pub enum Error { #[error("Failed to parse: `{}`", _0.user_display())] UvToml(PathBuf, #[source] Box), - #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `uv.toml` file. `{}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display(), _1, _1)] + #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `uv.toml` file. `{}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display(), _1, _1 + )] PyprojectOnlyField(PathBuf, &'static str), #[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")] @@ -574,6 +576,8 @@ pub struct EnvironmentOptions { pub python_install_registry: Option, pub install_mirrors: PythonInstallMirrors, pub log_context: Option, + pub http_timeout: Duration, + pub upload_http_timeout: Duration, #[cfg(feature = "tracing-durations-export")] pub tracing_durations_file: Option, } @@ -581,6 +585,15 @@ pub struct EnvironmentOptions { impl EnvironmentOptions { /// Create a new [`EnvironmentOptions`] from environment variables. pub fn new() -> Result { + // 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 http_timeout = parse_integer_environment_variable(EnvVars::UV_HTTP_TIMEOUT)? + .or(parse_integer_environment_variable( + EnvVars::UV_REQUEST_TIMEOUT, + )?) + .or(parse_integer_environment_variable(EnvVars::HTTP_TIMEOUT)?) + .map(Duration::from_secs); + Ok(Self { skip_wheel_filename_check: parse_boolish_environment_variable( EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK, @@ -601,6 +614,13 @@ impl EnvironmentOptions { )?, }, log_context: parse_boolish_environment_variable(EnvVars::UV_LOG_CONTEXT)?, + upload_http_timeout: parse_integer_environment_variable( + EnvVars::UV_UPLOAD_HTTP_TIMEOUT, + )? + .map(Duration::from_secs) + .or(http_timeout) + .unwrap_or(Duration::from_secs(15 * 60)), + http_timeout: http_timeout.unwrap_or(Duration::from_secs(30)), #[cfg(feature = "tracing-durations-export")] tracing_durations_file: parse_path_environment_variable( EnvVars::TRACING_DURATIONS_FILE, @@ -682,6 +702,34 @@ fn parse_string_environment_variable(name: &'static str) -> Result Result, Error> { + let value = match std::env::var(name) { + Ok(v) => v, + Err(e) => { + return match e { + std::env::VarError::NotPresent => Ok(None), + std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable { + name: name.to_string(), + value: err.to_string_lossy().to_string(), + err: "expected an integer".to_string(), + }), + }; + } + }; + if value.is_empty() { + return Ok(None); + } + match value.parse::() { + Ok(v) => Ok(Some(v)), + Err(_) => Err(Error::InvalidEnvironmentVariable { + name: name.to_string(), + value, + err: "expected an integer".to_string(), + }), + } +} + #[cfg(feature = "tracing-durations-export")] /// Parse a path environment variable. fn parse_path_environment_variable(name: &'static str) -> Option { diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index c7924c5c8..7468612d8 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -590,6 +590,9 @@ impl EnvVars { #[attr_added_in("0.1.38")] pub const NO_PROXY: &'static str = "NO_PROXY"; + /// Timeout (in seconds) for only upload HTTP requests. (default: 900 s) + pub const UV_UPLOAD_HTTP_TIMEOUT: &'static str = "UV_UPLOAD_HTTP_TIMEOUT"; + /// Timeout (in seconds) for HTTP requests. (default: 30 s) #[attr_added_in("0.1.7")] pub const UV_HTTP_TIMEOUT: &'static str = "UV_HTTP_TIMEOUT"; diff --git a/crates/uv/src/commands/auth/login.rs b/crates/uv/src/commands/auth/login.rs index 9fdc8d23d..57039b9fc 100644 --- a/crates/uv/src/commands/auth/login.rs +++ b/crates/uv/src/commands/auth/login.rs @@ -38,12 +38,15 @@ pub(crate) async fn login( bail!("Cannot specify a password when logging in to pyx"); } - let client = BaseClientBuilder::default() - .connectivity(network_settings.connectivity) - .native_tls(network_settings.native_tls) - .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .auth_integration(AuthIntegration::NoAuthMiddleware) - .build(); + let client = BaseClientBuilder::new( + network_settings.connectivity, + network_settings.native_tls, + network_settings.allow_insecure_host.clone(), + preview, + network_settings.timeout, + ) + .auth_integration(AuthIntegration::NoAuthMiddleware) + .build(); let access_token = pyx_login_with_browser(&pyx_store, &client, &printer).await?; let jwt = PyxJwt::decode(&access_token)?; diff --git a/crates/uv/src/commands/auth/logout.rs b/crates/uv/src/commands/auth/logout.rs index 0165fb693..74582b8c8 100644 --- a/crates/uv/src/commands/auth/logout.rs +++ b/crates/uv/src/commands/auth/logout.rs @@ -24,7 +24,7 @@ pub(crate) async fn logout( ) -> Result { let pyx_store = PyxTokenStore::from_settings()?; if pyx_store.is_known_domain(service.url()) { - return pyx_logout(&pyx_store, network_settings, printer).await; + return pyx_logout(&pyx_store, network_settings, printer, preview).await; } let backend = AuthBackend::from_settings(preview)?; @@ -95,13 +95,17 @@ async fn pyx_logout( store: &PyxTokenStore, network_settings: &NetworkSettings, printer: Printer, + preview: Preview, ) -> Result { // Initialize the client. - let client = BaseClientBuilder::default() - .connectivity(network_settings.connectivity) - .native_tls(network_settings.native_tls) - .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .build(); + let client = BaseClientBuilder::new( + network_settings.connectivity, + network_settings.native_tls, + network_settings.allow_insecure_host.clone(), + preview, + network_settings.timeout, + ) + .build(); // Retrieve the token store. let Some(tokens) = store.read().await? else { diff --git a/crates/uv/src/commands/auth/token.rs b/crates/uv/src/commands/auth/token.rs index 04d2aca01..ffd3547d1 100644 --- a/crates/uv/src/commands/auth/token.rs +++ b/crates/uv/src/commands/auth/token.rs @@ -27,13 +27,15 @@ pub(crate) async fn token( if username.is_some() { bail!("Cannot specify a username when logging in to pyx"); } - - let client = BaseClientBuilder::default() - .connectivity(network_settings.connectivity) - .native_tls(network_settings.native_tls) - .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .auth_integration(AuthIntegration::NoAuthMiddleware) - .build(); + let client = BaseClientBuilder::new( + network_settings.connectivity, + network_settings.native_tls, + network_settings.allow_insecure_host.clone(), + preview, + network_settings.timeout, + ) + .auth_integration(AuthIntegration::NoAuthMiddleware) + .build(); pyx_refresh(&pyx_store, &client, printer).await?; return Ok(ExitStatus::Success); diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 791ff754b..dea625cba 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -1,6 +1,5 @@ use std::fmt::Write; use std::sync::Arc; -use std::time::Duration; use anyhow::{Context, Result, bail}; use console::Term; @@ -18,6 +17,7 @@ use uv_publish::{ files_for_publishing, upload, }; use uv_redacted::DisplaySafeUrl; +use uv_settings::EnvironmentOptions; use uv_warnings::{warn_user_once, write_error_chain}; use crate::commands::reporters::PublishReporter; @@ -29,6 +29,7 @@ pub(crate) async fn publish( publish_url: DisplaySafeUrl, trusted_publishing: TrustedPublishing, keyring_provider: KeyringProviderType, + environment: &EnvironmentOptions, client_builder: &BaseClientBuilder<'_>, username: Option, password: Option, @@ -123,9 +124,7 @@ pub(crate) async fn publish( .keyring(keyring_provider) // Don't try cloning the request to make an unauthenticated request first. .auth_integration(AuthIntegration::OnlyAuthenticated) - // Set a very high timeout for uploads, connections are often 10x slower on upload than - // download. 15 min is taken from the time a trusted publishing token is valid. - .default_timeout(Duration::from_secs(15 * 60)) + .timeout(environment.upload_http_timeout) .build(); let oidc_client = client_builder .clone() diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 9dc667b7e..b97a22bd6 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -85,6 +85,9 @@ async fn run(mut cli: Cli) -> Result { .map(Cow::Owned) .unwrap_or_else(|| Cow::Borrowed(&*CWD)); + // Load environment variables not handled by Clap + let environment = EnvironmentOptions::new()?; + // The `--isolated` argument is deprecated on preview APIs, and warns on non-preview APIs. let deprecated_isolated = if cli.top_level.global_args.isolated { match &*cli.command { @@ -170,12 +173,17 @@ async fn run(mut cli: Cli) -> Result { .. }) = &mut **command { - let settings = GlobalSettings::resolve(&cli.top_level.global_args, filesystem.as_ref()); + let settings = GlobalSettings::resolve( + &cli.top_level.global_args, + filesystem.as_ref(), + &environment, + ); let client_builder = BaseClientBuilder::new( settings.network_settings.connectivity, settings.network_settings.native_tls, settings.network_settings.allow_insecure_host, settings.preview, + environment.http_timeout, ) .retries_from_env()?; Some( @@ -306,11 +314,12 @@ async fn run(mut cli: Cli) -> Result { .map(FilesystemOptions::from) .combine(filesystem); - // Load environment variables not handled by Clap - let environment = EnvironmentOptions::new()?; - // Resolve the global settings. - let globals = GlobalSettings::resolve(&cli.top_level.global_args, filesystem.as_ref()); + let globals = GlobalSettings::resolve( + &cli.top_level.global_args, + filesystem.as_ref(), + &environment, + ); // Resolve the cache settings. let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref()); @@ -447,6 +456,7 @@ async fn run(mut cli: Cli) -> Result { globals.network_settings.native_tls, globals.network_settings.allow_insecure_host.clone(), globals.preview, + environment.http_timeout, ) .retries_from_env()?; @@ -459,6 +469,7 @@ async fn run(mut cli: Cli) -> Result { args, &cli.top_level.global_args, filesystem.as_ref(), + &environment, ); show_settings!(args); @@ -481,6 +492,7 @@ async fn run(mut cli: Cli) -> Result { args, &cli.top_level.global_args, filesystem.as_ref(), + &environment, ); show_settings!(args); @@ -501,6 +513,7 @@ async fn run(mut cli: Cli) -> Result { args, &cli.top_level.global_args, filesystem.as_ref(), + &environment, ); show_settings!(args); @@ -1417,7 +1430,7 @@ async fn run(mut cli: Cli) -> Result { command: ToolCommand::Upgrade(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::ToolUpgradeSettings::resolve(args, filesystem, environment); + let args = settings::ToolUpgradeSettings::resolve(args, filesystem, &environment); show_settings!(args); // Initialize the cache. @@ -1680,6 +1693,7 @@ async fn run(mut cli: Cli) -> Result { publish_url, trusted_publishing, keyring_provider, + &environment, &client_builder, username, password, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 3da6397a7..3f5972bc9 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use std::path::PathBuf; use std::process; use std::str::FromStr; +use std::time::Duration; use uv_auth::Service; use uv_cache::{CacheArgs, Refresh}; @@ -78,8 +79,12 @@ pub(crate) struct GlobalSettings { impl GlobalSettings { /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration. - pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { - let network_settings = NetworkSettings::resolve(args, workspace); + pub(crate) fn resolve( + args: &GlobalArgs, + workspace: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, + ) -> Self { + let network_settings = NetworkSettings::resolve(args, workspace, environment); let python_preference = resolve_python_preference(args, workspace); Self { required_version: workspace @@ -172,10 +177,15 @@ pub(crate) struct NetworkSettings { pub(crate) connectivity: Connectivity, pub(crate) native_tls: bool, pub(crate) allow_insecure_host: Vec, + pub(crate) timeout: Duration, } impl NetworkSettings { - pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { + pub(crate) fn resolve( + args: &GlobalArgs, + workspace: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, + ) -> Self { let connectivity = if flag(args.offline, args.no_offline, "offline") .combine(workspace.and_then(|workspace| workspace.globals.offline)) .unwrap_or(false) @@ -184,6 +194,7 @@ impl NetworkSettings { } else { Connectivity::Online }; + let timeout = environment.http_timeout; let native_tls = flag(args.native_tls, args.no_native_tls, "native-tls") .combine(workspace.and_then(|workspace| workspace.globals.native_tls)) .unwrap_or(false); @@ -208,6 +219,7 @@ impl NetworkSettings { connectivity, native_tls, allow_insecure_host, + timeout, } } } @@ -746,7 +758,7 @@ impl ToolUpgradeSettings { pub(crate) fn resolve( args: ToolUpgradeArgs, filesystem: Option, - environment: EnvironmentOptions, + environment: &EnvironmentOptions, ) -> Self { let ToolUpgradeArgs { name, @@ -834,6 +846,7 @@ impl ToolUpgradeSettings { filesystem: top_level, install_mirrors: environment .install_mirrors + .clone() .combine(filesystem_install_mirrors), } } @@ -3662,11 +3675,12 @@ impl AuthLogoutSettings { args: AuthLogoutArgs, global_args: &GlobalArgs, filesystem: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, ) -> Self { Self { service: args.service, username: args.username, - network_settings: NetworkSettings::resolve(global_args, filesystem), + network_settings: NetworkSettings::resolve(global_args, filesystem, environment), } } } @@ -3687,11 +3701,12 @@ impl AuthTokenSettings { args: AuthTokenArgs, global_args: &GlobalArgs, filesystem: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, ) -> Self { Self { service: args.service, username: args.username, - network_settings: NetworkSettings::resolve(global_args, filesystem), + network_settings: NetworkSettings::resolve(global_args, filesystem, environment), } } } @@ -3714,13 +3729,14 @@ impl AuthLoginSettings { args: AuthLoginArgs, global_args: &GlobalArgs, filesystem: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, ) -> Self { Self { service: args.service, username: args.username, password: args.password, token: args.token, - network_settings: NetworkSettings::resolve(global_args, filesystem), + network_settings: NetworkSettings::resolve(global_args, filesystem, environment), } } } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 6f02086e1..85e89448b 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -128,6 +128,13 @@ impl TestContext { self } + /// Set the "http timeout" for all commands in this context. + pub fn with_http_timeout(mut self, http_timeout: &str) -> Self { + self.extra_env + .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into())); + self + } + /// Add extra standard filtering for messages like "Resolved 10 packages" which /// can differ between platforms. /// diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index b37952fa9..e47c6a2e2 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -66,6 +66,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -265,6 +266,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -465,6 +467,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -697,6 +700,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -898,6 +902,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -1075,6 +1080,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -1301,6 +1307,7 @@ fn resolve_index_url() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -1535,6 +1542,7 @@ fn resolve_index_url() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -1827,6 +1835,7 @@ fn resolve_find_links() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -2050,6 +2059,7 @@ fn resolve_top_level() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -2232,6 +2242,7 @@ fn resolve_top_level() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -2464,6 +2475,7 @@ fn resolve_top_level() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -2719,6 +2731,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -2891,6 +2904,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -3063,6 +3077,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -3237,6 +3252,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -3430,6 +3446,7 @@ fn resolve_tool() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -3615,6 +3632,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -3821,6 +3839,7 @@ fn resolve_both() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -4066,6 +4085,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -4390,6 +4410,7 @@ fn resolve_config_file() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -4689,6 +4710,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -4864,6 +4886,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -5058,6 +5081,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { port: None, }, ], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -5244,6 +5268,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -5478,6 +5503,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -5718,6 +5744,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -5953,6 +5980,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -6195,6 +6223,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -6430,6 +6459,7 @@ fn index_priority() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -6678,6 +6708,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -6843,6 +6874,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7006,6 +7038,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7171,6 +7204,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7334,6 +7368,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7498,6 +7533,7 @@ fn verify_hashes() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7677,6 +7713,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7789,6 +7826,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -7901,6 +7939,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8013,6 +8052,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8125,6 +8165,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8239,6 +8280,7 @@ fn preview_features() { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8372,6 +8414,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8545,6 +8588,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8741,6 +8785,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -8912,6 +8957,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9077,6 +9123,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9243,6 +9290,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9474,6 +9522,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9591,6 +9640,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9731,6 +9781,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9846,6 +9897,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -9951,6 +10003,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -10057,6 +10110,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -10227,6 +10281,7 @@ fn build_isolation_override() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, @@ -10395,6 +10450,7 @@ fn build_isolation_override() -> anyhow::Result<()> { connectivity: Online, native_tls: false, allow_insecure_host: [], + timeout: [TIME], }, concurrency: Concurrency { downloads: 50, diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index e4d980030..8244f3a45 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -882,6 +882,24 @@ fn seed_older_python_version() { context.venv.assert(predicates::path::is_dir()); } +#[test] +fn create_venv_with_invalid_http_timeout() { + let context = TestContext::new_with_versions(&["3.12"]).with_http_timeout("not_a_number"); + + // Create a virtual environment at `.venv`. + uv_snapshot!(context.filters(), context.venv() + .arg(context.venv.as_os_str()) + .arg("--python") + .arg("3.12"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse environment variable `UV_HTTP_TIMEOUT` with invalid value `not_a_number`: expected an integer + "###); +} + #[test] fn create_venv_unknown_python_minor() { let context = TestContext::new_with_versions(&["3.12"]).with_filtered_python_sources(); diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 11b67615c..4f3ee4b48 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -651,6 +651,10 @@ Equivalent to the `--torch-backend` command-line argument (e.g., `cpu`, `cu126`, Used ephemeral environments like CI to install uv to a specific path while preventing the installer from modifying shell profiles or environment variables. +### `UV_UPLOAD_HTTP_TIMEOUT` + +Timeout (in seconds) for only upload HTTP requests. (default: 900 s) + ### `UV_VENV_CLEAR` added in `0.8.0`