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