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) <[email protected]>
This commit is contained in:
@@ -12,6 +12,9 @@
|
|||||||
//! open is [`RemoteError::FileChanged`], never a mix of old and new bytes.
|
//! open is [`RemoteError::FileChanged`], never a mix of old and new bytes.
|
||||||
//! (A server that sends neither validator cannot be checked beyond the
|
//! (A server that sends neither validator cannot be checked beyond the
|
||||||
//! length; [`HttpOptions::require_validator`] refuses such servers.)
|
//! 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
|
//! - a server that ignores `Range` and answers `200` with the whole file is
|
||||||
//! refused with [`RemoteError::RangeNotSupported`], unless
|
//! refused with [`RemoteError::RangeNotSupported`], unless
|
||||||
//! [`HttpOptions::allow_full_download`] is set: then the file is
|
//! [`HttpOptions::allow_full_download`] is set: then the file is
|
||||||
@@ -508,15 +511,34 @@ impl HttpStorage {
|
|||||||
Ok((total, validator, bytes, false))
|
Ok((total, validator, bytes, false))
|
||||||
}
|
}
|
||||||
200 => {
|
200 => {
|
||||||
if !self.options.allow_full_download {
|
// RFC 9110 lets a server answer 200 when the range covers
|
||||||
return Err(RemoteError::RangeNotSupported(format!(
|
// the whole file: a body no longer than the range asked
|
||||||
|
// for is the whole file, ranges supported or not.
|
||||||
|
let want: Option<u64> =
|
||||||
|
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 \
|
"{} answered a range request with the whole file (status 200); set \
|
||||||
HttpOptions::allow_full_download to download it",
|
HttpOptions::allow_full_download to download it",
|
||||||
self.redactor.shown()
|
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| w > self.options.max_full_download) {
|
||||||
if want.is_some_and(|w: u64| w > self.options.max_full_download) {
|
|
||||||
return Err(RemoteError::Usage(format!(
|
return Err(RemoteError::Usage(format!(
|
||||||
"{}: the file is larger than HttpOptions::max_full_download ({} bytes)",
|
"{}: the file is larger than HttpOptions::max_full_download ({} bytes)",
|
||||||
self.redactor.shown(),
|
self.redactor.shown(),
|
||||||
|
|||||||
@@ -777,3 +777,26 @@ fn redirects_are_followed_safely() {
|
|||||||
let e = open_url_with(&front.url("/s.h5"), &opts).unwrap_err();
|
let e = open_url_with(&front.url("/s.h5"), &opts).unwrap_err();
|
||||||
assert!(matches!(e, Error::Remote(RemoteError::Redirect(_))), "{e}");
|
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}");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user