diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 2cc751bda..585167fdc 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -107,12 +107,14 @@ impl PythonInstallation { ) -> Result { let request = request.unwrap_or(&PythonRequest::Default); + let client = client_builder.build(); + let download_list = + ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; + // Python downloads are performing their own retries to catch stream errors, disable the // default retries to avoid the middleware performing uncontrolled retries. let retry_policy = client_builder.retry_policy(); let client = client_builder.clone().retries(0).build(); - let download_list = - ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; // Search for the installation let err = match Self::find( diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index e8d86ea73..96796dcda 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -295,7 +295,7 @@ pub(crate) async fn pip_compile( // Find an interpreter to use for building distributions let environment_preference = EnvironmentPreference::from_system_flag(system, false); let python_preference = python_preference.with_system_flag(system); - let client = client_builder.clone().retries(0).build(); + let client = client_builder.build(); let download_list = ManagedPythonDownloadList::new( &client, install_mirrors.python_downloads_json_url.as_deref(), diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 31e28e6a7..da6ea3efe 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -77,7 +77,7 @@ pub(crate) async fn find( ) .await?; - let client = client_builder.clone().retries(0).build(); + let client = client_builder.build(); let download_list = ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; let python = PythonInstallation::find( diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 6f04ef854..e0f0dabe4 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -97,7 +97,7 @@ pub(crate) async fn pin( for pin in file.versions() { writeln!(printer.stdout(), "{}", pin.to_canonical_string())?; if let Some(virtual_project) = &virtual_project { - let client = client_builder.clone().retries(0).build(); + let client = client_builder.build(); let download_list = ManagedPythonDownloadList::new( &client, install_mirrors.python_downloads_json_url.as_deref(), diff --git a/crates/uv/tests/it/network.rs b/crates/uv/tests/it/network.rs index 991ad27c3..3c1e441cd 100644 --- a/crates/uv/tests/it/network.rs +++ b/crates/uv/tests/it/network.rs @@ -421,3 +421,59 @@ async fn rfc9457_problem_details_license_violation() { ╰─▶ HTTP status client error (403 Forbidden) for url ([SERVER]/packages/tqdm-4.67.1-py3-none-any.whl) "); } + +/// Check the python list error message when the server returns HTTP status 500, a retryable error. +#[tokio::test] +async fn python_list_remote_python_downloads_json_url_http_500() { + let context = TestContext::new("3.12"); + + let (_server_drop_guard, mock_server_uri) = http_error_server().await; + + let python_downloads_json_url = format!("{mock_server_uri}/python_downloads.json"); + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .python_list() + .arg("--python-downloads-json-url") + .arg(&python_downloads_json_url) + .env_remove(EnvVars::UV_HTTP_RETRIES) + .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Error while fetching remote python downloads json from '[SERVER]/python_downloads.json' + Caused by: Request failed after 3 retries + Caused by: Failed to download [SERVER]/python_downloads.json + Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/python_downloads.json) + "); +} + +/// Check the python list error message when the server returns a retryable IO error. +#[tokio::test] +async fn python_list_remote_python_downloads_json_url_io_error() { + let context = TestContext::new("3.12"); + + let (_server_drop_guard, mock_server_uri) = io_error_server().await; + + let python_downloads_json_url = format!("{mock_server_uri}/python_downloads.json"); + let filters = vec![(mock_server_uri.as_str(), "[SERVER]")]; + uv_snapshot!(filters, context + .python_list() + .arg("--python-downloads-json-url") + .arg(&python_downloads_json_url) + .env_remove(EnvVars::UV_HTTP_RETRIES) + .env(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY, "true"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Error while fetching remote python downloads json from '[SERVER]/python_downloads.json' + Caused by: Failed to download [SERVER]/python_downloads.json + Caused by: Request failed after 3 retries + Caused by: error sending request for url ([SERVER]/python_downloads.json) + Caused by: client error (SendRequest) + Caused by: connection closed before message completed + "); +}