Skip to content

Commit 4a1813f

Browse files
authored
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 (hyperium/h2#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)
1 parent 580bc9d commit 4a1813f

3 files changed

Lines changed: 9 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async_zip = { workspace = true }
3838
bytecheck = { workspace = true }
3939
fs-err = { workspace = true, features = ["tokio"] }
4040
futures = { workspace = true }
41+
h2 = { workspace = true }
4142
html-escape = { workspace = true }
4243
http = { workspace = true }
4344
itertools = { workspace = true }

crates/uv-client/src/base_client.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ impl RetryableStrategy for UvRetryableStrategy {
983983
/// Whether the error looks like a network error that should be retried.
984984
///
985985
/// There are two cases that the default retry strategy is missing:
986-
/// * Inside the reqwest middleware error is an `io::Error` such as a broken pipe
986+
/// * Inside the reqwest or reqwest-middleware error is an `io::Error` such as a broken pipe
987987
/// * When streaming a response, a reqwest error may be hidden several layers behind errors
988988
/// of different crates processing the stream, including `io::Error` layers.
989989
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 {
10271027
}
10281028

10291029
trace!("Cannot retry nested reqwest error");
1030-
} else if let Some(io_err) = source.downcast_ref::<io::Error>() {
1030+
} else if let Some(io_err) = source.downcast_ref::<io::Error>().or_else(|| {
1031+
// h2 may hide an IO error inside.
1032+
source
1033+
.downcast_ref::<h2::Error>()
1034+
.and_then(|err| err.get_io())
1035+
}) {
10311036
has_known_error = true;
10321037
let retryable_io_err_kinds = [
10331038
// https://github.com/astral-sh/uv/issues/12054

0 commit comments

Comments
 (0)