Add UV_HTTP_RETRIES to customize retry counts (#14544)

I want to increase this number in CI and was surprised we didn't support
configuration yet.
This commit is contained in:
Zanie Blue 2025-07-11 07:35:27 -05:00 committed by GitHub
parent 2e0f399eeb
commit 71470b7b1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 143 additions and 1 deletions

View file

@ -6,6 +6,7 @@ use std::sync::Arc;
use std::time::Duration;
use std::{env, io, iter};
use anyhow::Context;
use anyhow::anyhow;
use http::{
HeaderMap, HeaderName, HeaderValue, Method, StatusCode,
@ -166,6 +167,25 @@ impl<'a> BaseClientBuilder<'a> {
self
}
/// Read the retry count from [`EnvVars::UV_HTTP_RETRIES`] if set, otherwise, make no change.
///
/// Errors when [`EnvVars::UV_HTTP_RETRIES`] is not a valid u32.
pub fn retries_from_env(self) -> anyhow::Result<Self> {
// TODO(zanieb): We should probably parse this in another layer, but there's not a natural
// fit for it right now
if let Some(value) = env::var_os(EnvVars::UV_HTTP_RETRIES) {
Ok(self.retries(
value
.to_string_lossy()
.as_ref()
.parse::<u32>()
.context("Failed to parse `UV_HTTP_RETRIES`")?,
))
} else {
Ok(self)
}
}
#[must_use]
pub fn native_tls(mut self, native_tls: bool) -> Self {
self.native_tls = native_tls;
@ -238,7 +258,11 @@ impl<'a> BaseClientBuilder<'a> {
/// Create a [`RetryPolicy`] for the client.
fn retry_policy(&self) -> ExponentialBackoff {
ExponentialBackoff::builder().build_with_max_retries(self.retries)
let mut builder = ExponentialBackoff::builder();
if env::var_os(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY).is_some() {
builder = builder.retry_bounds(Duration::from_millis(0), Duration::from_millis(0));
}
builder.build_with_max_retries(self.retries)
}
pub fn build(&self) -> BaseClient {

View file

@ -115,6 +115,11 @@ impl<'a> RegistryClientBuilder<'a> {
self
}
pub fn retries_from_env(mut self) -> anyhow::Result<Self> {
self.base_client_builder = self.base_client_builder.retries_from_env()?;
Ok(self)
}
#[must_use]
pub fn native_tls(mut self, native_tls: bool) -> Self {
self.base_client_builder = self.base_client_builder.native_tls(native_tls);

View file

@ -31,6 +31,9 @@ pub enum Error {
#[error(transparent)]
WheelFilename(#[from] uv_distribution_filename::WheelFilenameError),
#[error("Failed to construct HTTP client")]
ClientError(#[source] anyhow::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}

View file

@ -402,6 +402,9 @@ impl EnvVars {
/// Timeout (in seconds) for HTTP requests. (default: 30 s)
pub const UV_HTTP_TIMEOUT: &'static str = "UV_HTTP_TIMEOUT";
/// The number of retries for HTTP requests. (default: 3)
pub const UV_HTTP_RETRIES: &'static str = "UV_HTTP_RETRIES";
/// Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`.
pub const UV_REQUEST_TIMEOUT: &'static str = "UV_REQUEST_TIMEOUT";
@ -659,6 +662,9 @@ impl EnvVars {
#[attr_hidden]
pub const UV_TEST_VENDOR_LINKS_URL: &'static str = "UV_TEST_VENDOR_LINKS_URL";
/// Used to disable delay for HTTP retries in tests.
pub const UV_TEST_NO_HTTP_RETRY_DELAY: &'static str = "UV_TEST_NO_HTTP_RETRY_DELAY";
/// Used to set an index url for tests.
#[attr_hidden]
pub const UV_TEST_INDEX_URL: &'static str = "UV_TEST_INDEX_URL";

View file

@ -207,6 +207,7 @@ async fn build_impl(
} = settings;
let client_builder = BaseClientBuilder::default()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -179,6 +179,7 @@ pub(crate) async fn pip_compile(
}
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -99,6 +99,7 @@ pub(crate) async fn pip_install(
let start = std::time::Instant::now();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -87,6 +87,7 @@ pub(crate) async fn pip_list(
let capabilities = IndexCapabilities::default();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -81,6 +81,7 @@ pub(crate) async fn pip_sync(
preview: PreviewMode,
) -> Result<ExitStatus> {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -86,6 +86,7 @@ pub(crate) async fn pip_tree(
let capabilities = IndexCapabilities::default();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -42,6 +42,7 @@ pub(crate) async fn pip_uninstall(
let start = std::time::Instant::now();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -176,6 +176,7 @@ pub(crate) async fn add(
}
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -329,6 +330,7 @@ pub(crate) async fn add(
.ok();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(settings.resolver.keyring_provider)

View file

@ -218,6 +218,7 @@ async fn init_script(
warn_user_once!("`--package` is a no-op for Python scripts, which are standalone");
}
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -348,6 +349,7 @@ async fn init_project(
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -99,6 +99,7 @@ pub(crate) async fn lock(
let script = match script {
Some(ScriptPath::Path(path)) => {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -588,6 +589,7 @@ async fn do_lock(
// Initialize the client.
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(*keyring_provider)

View file

@ -690,6 +690,7 @@ impl ScriptInterpreter {
}
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -946,6 +947,7 @@ impl ProjectInterpreter {
}
let client_builder = BaseClientBuilder::default()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -1656,6 +1658,8 @@ pub(crate) async fn resolve_names(
} = settings;
let client_builder = BaseClientBuilder::new()
.retries_from_env()
.map_err(uv_requirements::Error::ClientError)?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(*keyring_provider)
@ -1813,6 +1817,7 @@ pub(crate) async fn resolve_environment(
} = spec.requirements;
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(*keyring_provider)
@ -1984,6 +1989,7 @@ pub(crate) async fn sync_environment(
} = settings;
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)
@ -2147,6 +2153,7 @@ pub(crate) async fn update_environment(
} = settings;
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(*keyring_provider)

View file

@ -618,6 +618,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
// If we're isolating the environment, use an ephemeral virtual environment as the
// base environment for the project.
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -859,6 +860,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
let interpreter = {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -929,6 +931,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
None
} else {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -1526,6 +1529,7 @@ impl RunCommand {
.tempfile()?;
let client = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone())

View file

@ -623,6 +623,7 @@ pub(super) async fn do_sync(
} = settings;
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)

View file

@ -215,6 +215,7 @@ pub(crate) async fn tree(
let client = RegistryClientBuilder::new(
cache.clone().with_refresh(Refresh::All(Timestamp::now())),
)
.retries_from_env()?
.native_tls(network_settings.native_tls)
.connectivity(network_settings.connectivity)
.allow_insecure_host(network_settings.allow_insecure_host.clone())

View file

@ -95,6 +95,7 @@ pub(crate) async fn publish(
false,
);
let registry_client_builder = RegistryClientBuilder::new(cache.clone())
.retries_from_env()?
.native_tls(network_settings.native_tls)
.connectivity(network_settings.connectivity)
.allow_insecure_host(network_settings.allow_insecure_host.clone())

View file

@ -376,6 +376,7 @@ pub(crate) async fn install(
// Download and unpack the Python versions concurrently
let client = uv_client::BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone())

View file

@ -107,6 +107,7 @@ pub(crate) async fn pin(
}
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -66,6 +66,7 @@ pub(crate) async fn install(
preview: PreviewMode,
) -> Result<ExitStatus> {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
@ -97,6 +98,7 @@ pub(crate) async fn install(
let workspace_cache = WorkspaceCache::default();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -690,6 +690,7 @@ async fn get_or_create_environment(
preview: PreviewMode,
) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -80,6 +80,7 @@ pub(crate) async fn upgrade(
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());

View file

@ -193,6 +193,9 @@ async fn venv_impl(
.unwrap_or(PathBuf::from(".venv")),
);
// TODO(zanieb): We don't use [`BaseClientBuilder::retries_from_env`] here because it's a pain
// to map into a miette diagnostic. We should just remove miette diagnostics here, we're not
// using them elsewhere.
let client_builder = BaseClientBuilder::default()
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)

View file

@ -499,6 +499,66 @@ fn install_package() {
context.assert_command("import flask").success();
}
#[tokio::test]
async fn install_http_retries() {
let context = TestContext::new("3.12");
let server = MockServer::start().await;
// Create a server that always fails, so we can see the number of retries used
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
uv_snapshot!(context.filters(), context.pip_install()
.arg("anyio")
.arg("--index")
.arg(server.uri())
.env(EnvVars::UV_HTTP_RETRIES, "foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `UV_HTTP_RETRIES`
Caused by: invalid digit found in string
"
);
uv_snapshot!(context.filters(), context.pip_install()
.arg("anyio")
.arg("--index")
.arg(server.uri())
.env(EnvVars::UV_HTTP_RETRIES, "999999999999"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `UV_HTTP_RETRIES`
Caused by: number too large to fit in target type
"
);
uv_snapshot!(context.filters(), context.pip_install()
.arg("anyio")
.arg("--index")
.arg(server.uri())
.env(EnvVars::UV_HTTP_RETRIES, "5")
.env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Request failed after 5 retries
Caused by: Failed to fetch: `http://[LOCALHOST]/anyio/`
Caused by: HTTP status server error (503 Service Unavailable) for url (http://[LOCALHOST]/anyio/)
"
);
}
/// Install a package from a `requirements.txt` into a virtual environment.
#[test]
fn install_requirements_txt() -> Result<()> {

View file

@ -102,6 +102,10 @@ Equivalent to the `--token` argument for self update. A GitHub token for authent
Enables fetching files stored in Git LFS when installing a package from a Git repository.
### `UV_HTTP_RETRIES`
The number of retries for HTTP requests. (default: 3)
### `UV_HTTP_TIMEOUT`
Timeout (in seconds) for HTTP requests. (default: 30 s)
@ -416,6 +420,10 @@ WARNING: `UV_SYSTEM_PYTHON=true` is intended for use in continuous integration (
or containerized environments and should be used with caution, as modifying the system
Python can lead to unexpected behavior.
### `UV_TEST_NO_HTTP_RETRY_DELAY`
Used to disable delay for HTTP retries in tests.
### `UV_TOOL_BIN_DIR`
Specifies the "bin" directory for installing tool executables.