From 4a1813f228f818479d074fc9b2364b7ec8414963 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 4 Sep 2025 14:45:00 +0200 Subject: [PATCH] Extract IO errors from h2 for streaming retries of Connection Reset (#15675) Our streaming retries were missing connection reset errors as h2 was shadowing IO errors (https://github.com/hyperium/h2/issues/862). **Test plan** In one terminal: ``` cargo python uninstall 3.12 && cargo run python install 3.12 -vv ``` In another: ``` sudo tcpkill -i wlp2s0 port 443 ``` Output: ``` error: Failed to install cpython-3.12.11-linux-x86_64-gnu Caused by: Request failed after 3 retries Caused by: Failed to download https://github.com/astral-sh/python-build-standalone/releases/download/20250902/cpython-3.12.11%2B20250902-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz Caused by: error sending request for url (https://github.com/astral-sh/python-build-standalone/releases/download/20250902/cpython-3.12.11%2B20250902-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz) Caused by: client error (SendRequest) Caused by: connection error Caused by: connection reset ``` I don't know how to test that from inside Rust. Fix #14171 (again, hopefully) --- Cargo.lock | 1 + crates/uv-client/Cargo.toml | 1 + crates/uv-client/src/base_client.rs | 9 +++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91a5c213c..6b0a535dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5380,6 +5380,7 @@ dependencies = [ "bytecheck", "fs-err", "futures", + "h2", "html-escape", "http", "http-body-util", diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index e1a933241..73409d6aa 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -38,6 +38,7 @@ async_zip = { workspace = true } bytecheck = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } +h2 = { workspace = true } html-escape = { workspace = true } http = { workspace = true } itertools = { workspace = true } diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 20ef8f7b6..0e4942f78 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -983,7 +983,7 @@ impl RetryableStrategy for UvRetryableStrategy { /// Whether the error looks like a network error that should be retried. /// /// There are two cases that the default retry strategy is missing: -/// * Inside the reqwest middleware error is an `io::Error` such as a broken pipe +/// * Inside the reqwest or reqwest-middleware error is an `io::Error` such as a broken pipe /// * When streaming a response, a reqwest error may be hidden several layers behind errors /// of different crates processing the stream, including `io::Error` layers. pub fn is_transient_network_error(err: &(dyn Error + 'static)) -> bool { @@ -1027,7 +1027,12 @@ pub fn is_transient_network_error(err: &(dyn Error + 'static)) -> bool { } trace!("Cannot retry nested reqwest error"); - } else if let Some(io_err) = source.downcast_ref::() { + } else if let Some(io_err) = source.downcast_ref::().or_else(|| { + // h2 may hide an IO error inside. + source + .downcast_ref::() + .and_then(|err| err.get_io()) + }) { has_known_error = true; let retryable_io_err_kinds = [ // https://github.com/astral-sh/uv/issues/12054