Handle broken range request implementations (#18780)

This updates async_http_range_reader to v0.11.0 to add the missing range
request bounds validation:
https://github.com/astral-sh/async_http_range_reader/pull/8.

An open question is how we want to behave when the server has an
incorrect range request implementation (while advertising range request
support). In the current implementation, it warns with the index URL, so
that the user is aware that the massive slowdown is caused by a server
advertising broken features.

Also removes a dependency where the corresponding repo was deleted.

Fixes https://github.com/astral-sh/uv/issues/18316
This commit is contained in:
konsti
2026-04-01 12:03:31 +02:00
committed by GitHub
parent 0c1d0f7c80
commit 5b369e6370
4 changed files with 56 additions and 23 deletions
Generated
+2 -9
View File
@@ -326,12 +326,11 @@ dependencies = [
[[package]]
name = "astral_async_http_range_reader"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9a05148c7b3e85c17806fef4b2889c44f1a60575c5da9e9be80ce13227185"
checksum = "4a8647866aee8d9707ae6ccc35205803a6df47c0ba83c5339ea6061b79131e4f"
dependencies = [
"astral-reqwest-middleware",
"bisection",
"futures",
"http-content-range",
"itertools 0.14.0",
@@ -548,12 +547,6 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bisection"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e079a1bab0ecce6cf4b4b74c0c37afa4a697136eb3b127875c84a8f04a8c3"
[[package]]
name = "bitflags"
version = "1.3.2"
+1 -1
View File
@@ -104,7 +104,7 @@ async-compression = { version = "0.4.12", features = [
"zstd",
] }
async-trait = { version = "0.1.82" }
async_http_range_reader = { version = "0.10.0", package = "astral_async_http_range_reader" }
async_http_range_reader = { version = "0.11.0", package = "astral_async_http_range_reader" }
async_zip = { version = "0.0.17", package = "astral_async_zip", features = [
"bzip2",
"deflate",
+52 -12
View File
@@ -9,13 +9,13 @@ use reqwest::Response;
use serde::Deserialize;
use tracing::warn;
use uv_cache::Error as CacheError;
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;
use crate::middleware::OfflineError;
use crate::{FlatIndexError, html};
use uv_cache::Error as CacheError;
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_distribution_types::IndexUrl;
use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;
/// RFC 9457 Problem Details for HTTP APIs
///
@@ -242,7 +242,11 @@ impl Error {
}
/// Returns `true` if the error is due to the server not supporting HTTP range requests.
pub fn is_http_range_requests_unsupported(&self) -> bool {
pub fn is_http_range_requests_unsupported(
&self,
url: &DisplaySafeUrl,
index: Option<&IndexUrl>,
) -> bool {
match &*self.kind {
// The server doesn't support range requests (as reported by the `HEAD` check).
ErrorKind::AsyncHttpRangeReader(
@@ -261,6 +265,25 @@ impl Error {
return true;
}
// The server advertises range request support, but doesn't implement it correctly.
ErrorKind::AsyncHttpRangeReader(
_,
AsyncHttpRangeReaderError::RangeMismatch { .. }
| AsyncHttpRangeReaderError::ResponseTooShort { .. }
| AsyncHttpRangeReaderError::ResponseTooLong { .. },
) => {
let url = if let Some(index) = index {
index.url()
} else {
url
};
warn!(
"Invalid range request response from server that declares HTTP range request \
support, falling back to streaming: {url}"
);
return true;
}
// The server returned a "Method Not Allowed" error, indicating it doesn't support
// HEAD requests, so we can't check for range requests.
ErrorKind::WrappedReqwestError(_, err) => {
@@ -295,14 +318,31 @@ impl Error {
// The server doesn't support range requests, but we only discovered this while
// unzipping due to erroneous server behavior.
ErrorKind::Zip(_, ZipError::UpstreamReadError(err)) => {
if let Some(inner) = err.get_ref() {
if let Some(inner) = inner.downcast_ref::<AsyncHttpRangeReaderError>() {
if matches!(
inner,
AsyncHttpRangeReaderError::HttpRangeRequestUnsupported
) {
if let Some(inner) = err.get_ref()
&& let Some(range_reader_error) =
inner.downcast_ref::<AsyncHttpRangeReaderError>()
{
match range_reader_error {
AsyncHttpRangeReaderError::HttpRangeRequestUnsupported
| AsyncHttpRangeReaderError::ContentLengthMissing
| AsyncHttpRangeReaderError::ContentRangeMissing => {
return true;
}
AsyncHttpRangeReaderError::RangeMismatch { .. }
| AsyncHttpRangeReaderError::ResponseTooShort { .. }
| AsyncHttpRangeReaderError::ResponseTooLong { .. } => {
let url = if let Some(index) = index {
index.url()
} else {
url
};
warn!(
"Invalid range request response from server that declares HTTP \
range request support, falling back to streaming: {url}"
);
return true;
}
_ => {}
}
}
}
+1 -1
View File
@@ -1191,7 +1191,7 @@ impl RegistryClient {
match result {
Ok(metadata) => return Ok(metadata),
Err(err) => {
if err.is_http_range_requests_unsupported() {
if err.is_http_range_requests_unsupported(url, index) {
// The range request version failed. Fall back to streaming the file to search
// for the METADATA file.
warn!("Range requests not supported for {filename}; streaming wheel");