From 61e34927dce32be401a6a563608c1875b4b1582c Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 18:32:15 -0500 Subject: [PATCH] clawhdf5-remote: a 200 covering the requested range is the whole file The first request asks for bytes=0-1048575. RFC 9110 lets a server answer 200 when the range covers the whole representation, so a file under 1 MiB on a server that does support ranges could be refused as 'does not support range requests'. A 200 whose Content-Length (or, without one, its body, read at most that far) is within the range asked for is now kept as the whole file and read from memory; a longer one is still refused unless allow_full_download is set. Test: a 9968-byte file served with 200 opens in one request with the transcript of File::open (it was refused before); with a 4096-byte first request it is still refused. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/clawhdf5-remote/src/http.rs | 32 +++++++++++++++++++++++----- crates/clawhdf5-remote/tests/http.rs | 23 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/clawhdf5-remote/src/http.rs b/crates/clawhdf5-remote/src/http.rs index 9fd10c6..67b5612 100644 --- a/crates/clawhdf5-remote/src/http.rs +++ b/crates/clawhdf5-remote/src/http.rs @@ -12,6 +12,9 @@ //! open is [`RemoteError::FileChanged`], never a mix of old and new bytes. //! (A server that sends neither validator cannot be checked beyond the //! length; [`HttpOptions::require_validator`] refuses such servers.) +//! - a `200` answer to the first request whose body is no longer than the +//! range asked for is the whole file (a server may answer so when the +//! range covers it): it is kept and read from memory. //! - a server that ignores `Range` and answers `200` with the whole file is //! refused with [`RemoteError::RangeNotSupported`], unless //! [`HttpOptions::allow_full_download`] is set: then the file is @@ -508,15 +511,34 @@ impl HttpStorage { Ok((total, validator, bytes, false)) } 200 => { - if !self.options.allow_full_download { - return Err(RemoteError::RangeNotSupported(format!( + // RFC 9110 lets a server answer 200 when the range covers + // the whole file: a body no longer than the range asked + // for is the whole file, ranges supported or not. + let want: Option = + header(&resp, "content-length").and_then(|v| v.trim().parse().ok()); + let refused = || { + RemoteError::RangeNotSupported(format!( "{} answered a range request with the whole file (status 200); set \ HttpOptions::allow_full_download to download it", self.redactor.shown() - ))); + )) + }; + if !self.options.allow_full_download { + return match want { + Some(w) if w <= n => { + let bytes = self.body(resp, Some(w), 0)?; + Ok((bytes.len() as u64, validator, bytes, true)) + } + Some(_) => Err(refused()), + // No length: read at most the range asked for. + None => match self.body(resp, None, n) { + Ok(bytes) => Ok((bytes.len() as u64, validator, bytes, true)), + Err(RemoteError::Usage(_)) => Err(refused()), + Err(e) => Err(e), + }, + }; } - let want = header(&resp, "content-length").and_then(|v| v.trim().parse().ok()); - if want.is_some_and(|w: u64| w > self.options.max_full_download) { + if want.is_some_and(|w| w > self.options.max_full_download) { return Err(RemoteError::Usage(format!( "{}: the file is larger than HttpOptions::max_full_download ({} bytes)", self.redactor.shown(), diff --git a/crates/clawhdf5-remote/tests/http.rs b/crates/clawhdf5-remote/tests/http.rs index 08119f3..679f977 100644 --- a/crates/clawhdf5-remote/tests/http.rs +++ b/crates/clawhdf5-remote/tests/http.rs @@ -777,3 +777,26 @@ fn redirects_are_followed_safely() { let e = open_url_with(&front.url("/s.h5"), &opts).unwrap_err(); assert!(matches!(e, Error::Remote(RemoteError::Redirect(_))), "{e}"); } + +/// A server may answer the first request (bytes 0 to 1 MiB - 1) with 200 +/// when that covers the whole file: a body no longer than the range asked +/// for is accepted as the whole file, in one request. +#[test] +fn a_200_covering_the_requested_range_is_the_whole_file() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let tall = root.join("../clawhdf5/tests/fixtures/tall.h5"); + let bytes = std::fs::read(&tall).unwrap(); + assert!(bytes.len() < 1 << 20); + let server = Server::start(vec![("/t.h5".into(), bytes.clone())]); + server.shared.ignore_range.store(true, Ordering::SeqCst); + let storage = storage_for_url(&server.url("/t.h5"), &quick()).unwrap(); + let f = File::open_storage(storage).unwrap(); + assert_eq!(f.contiguous_bytes(), Some(&bytes[..])); + assert_eq!(transcript(&f), transcript(&File::open(&tall).unwrap())); + assert_eq!(server.requests(), 1); + // Larger than the range asked for: still refused. + let mut opts = quick().http; + opts.first_request = 4096; + let e = HttpStorage::open(&server.url("/t.h5"), opts).unwrap_err(); + assert!(matches!(e, RemoteError::RangeNotSupported(_)), "{e}"); +}