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)
This commit is contained in:
konsti
2025-09-04 14:45:00 +02:00
committed by GitHub
parent 580bc9d079
commit 4a1813f228
3 changed files with 9 additions and 2 deletions
Generated
+1
View File
@@ -5380,6 +5380,7 @@ dependencies = [
"bytecheck",
"fs-err",
"futures",
"h2",
"html-escape",
"http",
"http-body-util",
+1
View File
@@ -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 }
+7 -2
View File
@@ -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::<io::Error>() {
} else if let Some(io_err) = source.downcast_ref::<io::Error>().or_else(|| {
// h2 may hide an IO error inside.
source
.downcast_ref::<h2::Error>()
.and_then(|err| err.get_io())
}) {
has_known_error = true;
let retryable_io_err_kinds = [
// https://github.com/astral-sh/uv/issues/12054