Check nested IO errors for retries (#13260)

## Summary

The only thing that changed for #12175 relevant to the existing
downloads is the order of nesting, so we're checking all nested IO
errors instead of only the first one.

See #13238

## Test Plan

This is an educated guess based on what happens if I turn off the
network during a download.

```
Downloading cpython-3.13.3-linux-x86_64-gnu (download) (20.3MiB)
TRACE Considering retry of error: ExtractError("cpython-3.13.3-20250409-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", Io(Custom { kind: Other, error: TarError { desc: "failed to unpack `/home/konsti/.local/share/uv/python/.temp/.tmpe3AIvt/python/lib/libpython3.13.so.1.0`", io: Custom { kind: Other, error: TarError { desc: "failed to unpack `python/lib/libpython3.13.so.1.0` into `/home/konsti/.local/share/uv/python/.temp/.tmpe3AIvt/python/lib/libpython3.13.so.1.0`", io: Custom { kind: Other, error: reqwest::Error { kind: Decode, source: reqwest::Error { kind: Body, source: TimedOut } } } } } } }))
TRACE Cannot retry IO error: not one of `ConnectionReset` or `UnexpectedEof`
TRACE Cannot retry IO error: not one of `ConnectionReset` or `UnexpectedEof`
TRACE Cannot retry error: not an IO error
error: Failed to install cpython-3.13.3-linux-x86_64-gnu
  Caused by: Failed to extract archive: cpython-3.13.3-20250409-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz
  Caused by: failed to unpack `/home/konsti/.local/share/uv/python/.temp/.tmpe3AIvt/python/lib/libpython3.13.so.1.0`
  Caused by: failed to unpack `python/lib/libpython3.13.so.1.0` into `/home/konsti/.local/share/uv/python/.temp/.tmpe3AIvt/python/lib/libpython3.13.so.1.0`
  Caused by: error decoding response body
  Caused by: request or response body error
  Caused by: operation timed out
```
This commit is contained in:
konsti 2025-05-02 14:41:09 +02:00 committed by GitHub
parent 801fd0e5b8
commit 360a335e7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4,7 +4,7 @@ use std::fmt::Write;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{env, iter};
use std::{env, io, iter};
use itertools::Itertools;
use reqwest::{Client, ClientBuilder, Proxy, Response};
@ -493,18 +493,18 @@ pub fn is_extended_transient_error(err: &dyn Error) -> bool {
trace!("Considering retry of error: {err:?}");
}
if let Some(io) = find_source::<std::io::Error>(&err) {
if io.kind() == std::io::ErrorKind::ConnectionReset
|| io.kind() == std::io::ErrorKind::UnexpectedEof
// IO Errors may be nested through custom IO errors.
for io_err in find_sources::<io::Error>(&err) {
if io_err.kind() == io::ErrorKind::ConnectionReset
|| io_err.kind() == io::ErrorKind::UnexpectedEof
{
trace!("Retrying error: `ConnectionReset` or `UnexpectedEof`");
return true;
}
trace!("Cannot retry error: not one of `ConnectionReset` or `UnexpectedEof`");
} else {
trace!("Cannot retry error: not an IO error");
trace!("Cannot retry IO error: not one of `ConnectionReset` or `UnexpectedEof`");
}
trace!("Cannot retry error: not an IO error");
false
}
@ -521,3 +521,12 @@ fn find_source<E: Error + 'static>(orig: &dyn Error) -> Option<&E> {
}
None
}
/// Return all errors in the chain of a specific type.
///
/// This handles cases such as nested `io::Error`s.
///
/// See <https://github.com/seanmonstar/reqwest/issues/1602#issuecomment-1220996681>
fn find_sources<E: Error + 'static>(orig: &dyn Error) -> impl Iterator<Item = &E> {
iter::successors(find_source::<E>(orig), |&err| find_source(err))
}