use std::io::Read;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use crate::RangeReader;
pub const WHOLE_BODY_CAP: u64 = 256 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FetchPolicy {
pub concurrency: usize,
pub coalesce_gap: u64,
pub max_span: u64,
pub retries: u32,
pub backoff_ms: u64,
pub trace: bool,
pub whole_body_cap: u64,
}
impl FetchPolicy {
pub const DEFAULT: FetchPolicy = FetchPolicy {
concurrency: 16,
coalesce_gap: 32 << 10,
max_span: 8 << 20,
retries: 2,
backoff_ms: 100,
trace: false,
whole_body_cap: WHOLE_BODY_CAP,
};
pub fn from_env() -> Self {
let num = |k: &str| {
std::env::var(k)
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
};
let d = Self::DEFAULT;
FetchPolicy {
concurrency: num("RETE_HTTP_CONCURRENCY")
.map_or(d.concurrency, |n| n.clamp(1, 64) as usize),
coalesce_gap: num("RETE_HTTP_COALESCE_KB")
.map_or(d.coalesce_gap, |kb| kb.saturating_mul(1024)),
max_span: d.max_span,
retries: num("RETE_HTTP_RETRIES").map_or(d.retries, |n| n.min(10) as u32),
backoff_ms: d.backoff_ms,
trace: std::env::var_os("RETE_HTTP_TRACE").is_some(),
whole_body_cap: d.whole_body_cap,
}
}
fn backoff(&self, attempt: u32) -> Duration {
let ms = self
.backoff_ms
.saturating_mul(4u64.saturating_pow(attempt))
.min(2000);
Duration::from_millis(ms)
}
}
impl Default for FetchPolicy {
fn default() -> Self {
Self::DEFAULT
}
}
enum Failure {
Transient(std::io::Error),
Permanent(std::io::Error),
}
fn retryable_status(code: u16) -> bool {
code == 408 || code == 429 || ((500..600).contains(&code) && code != 501 && code != 505)
}
fn classify(err: ureq::Error, what: &str) -> Failure {
match err {
ureq::Error::Status(code, _) => {
let e = std::io::Error::other(format!("HTTP {code} for {what}"));
if retryable_status(code) {
Failure::Transient(e)
} else {
Failure::Permanent(e)
}
}
ureq::Error::Transport(t) => {
let transient = matches!(
t.kind(),
ureq::ErrorKind::Io
| ureq::ErrorKind::ConnectionFailed
| ureq::ErrorKind::Dns
| ureq::ErrorKind::ProxyConnect
);
let e = std::io::Error::other(format!("{t} ({what})"));
if transient {
Failure::Transient(e)
} else {
Failure::Permanent(e)
}
}
}
}
fn content_range_start(v: &str) -> Option<u64> {
let v = v.trim().strip_prefix("bytes")?.trim_start();
v.split(['-', '/']).next()?.trim().parse().ok()
}
fn content_range_total(v: &str) -> Option<u64> {
v.rsplit('/').next()?.trim().parse().ok()
}
#[derive(Debug, PartialEq, Eq)]
struct Span {
offset: u64,
len: u64,
}
type Placement = Vec<Option<(usize, u64)>>;
fn coalesce(
ranges: &[(u64, u64)],
gap: u64,
max_span: u64,
) -> std::io::Result<(Vec<Span>, Placement)> {
let mut order: Vec<usize> = (0..ranges.len()).filter(|&i| ranges[i].1 > 0).collect();
for &i in &order {
let (o, l) = ranges[i];
if o.checked_add(l).is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("range {o}+{l} overflows"),
));
}
}
order.sort_by_key(|&i| ranges[i]);
let mut spans: Vec<Span> = Vec::new();
let mut place: Placement = vec![None; ranges.len()];
for i in order {
let (o, l) = ranges[i];
let end = o + l;
let joins = spans.last().is_some_and(|s| {
let s_end = s.offset + s.len;
o <= s_end.saturating_add(gap) && end.max(s_end) - s.offset <= max_span
});
if joins {
let s = spans.last_mut().expect("checked above");
s.len = (s.offset + s.len).max(end) - s.offset;
} else {
spans.push(Span { offset: o, len: l });
}
let si = spans.len() - 1;
place[i] = Some((si, o - spans[si].offset));
}
Ok((spans, place))
}
fn whole_body_refusal(url: &str, bytes: Option<u64>, cap: u64) -> std::io::Error {
let size = match bytes {
Some(n) => format!("all {n} bytes"),
None => format!("more than {cap} bytes"),
};
std::io::Error::other(format!(
"the host ignored the Range header and sent {size} of {url} (status 200 instead \
of 206 Partial Content); a full body over {} MiB is not kept in memory — host the \
file where HTTP range requests work",
cap >> 20
))
}
fn slice_whole(whole: &[u8], offset: u64, len: u64, url: &str) -> std::io::Result<Vec<u8>> {
let end = offset
.checked_add(len)
.filter(|&end| end <= whole.len() as u64)
.ok_or_else(|| {
std::io::Error::other(format!(
"range {offset}+{len} is past the end of the {} bytes {url} sent",
whole.len()
))
})?;
Ok(whole[offset as usize..end as usize].to_vec())
}
fn read_capped(resp: ureq::Response, cap: u64, what: &str) -> Result<Option<Vec<u8>>, Failure> {
let declared = resp
.header("content-length")
.and_then(|v| v.trim().parse::<u64>().ok());
if declared.is_some_and(|n| n > cap) {
return Ok(None); }
let mut buf = Vec::with_capacity(declared.unwrap_or(0).min(cap) as usize);
resp.into_reader()
.take(cap.saturating_add(1))
.read_to_end(&mut buf)
.map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
if buf.len() as u64 > cap {
return Ok(None);
}
if declared.is_some_and(|n| n != buf.len() as u64) {
return Err(Failure::Transient(std::io::Error::other(format!(
"short body: got {} of the {} bytes {what} declared",
buf.len(),
declared.unwrap_or(0)
))));
}
Ok(Some(buf))
}
struct Probe {
len: u64,
whole: Option<Vec<u8>>,
}
pub struct HttpRangeReader {
url: String,
len: u64,
agent: ureq::Agent,
policy: FetchPolicy,
headers: Vec<(String, String)>,
whole: OnceLock<Vec<u8>>,
adopting: Mutex<()>,
}
impl HttpRangeReader {
pub fn open(url: &str) -> std::io::Result<Self> {
Self::open_with(url, FetchPolicy::from_env())
}
pub fn open_with(url: &str, policy: FetchPolicy) -> std::io::Result<Self> {
Self::open_with_headers(url, Vec::new(), policy)
}
pub fn open_with_headers(
url: &str,
headers: Vec<(String, String)>,
policy: FetchPolicy,
) -> std::io::Result<Self> {
let agent = ureq::AgentBuilder::new()
.max_idle_connections(policy.concurrency.max(4) * 2)
.max_idle_connections_per_host(policy.concurrency)
.timeout_connect(Duration::from_secs(30))
.timeout_read(Duration::from_secs(120))
.build();
let mut reader = HttpRangeReader {
url: url.to_string(),
len: 0,
agent,
policy,
headers,
whole: OnceLock::new(),
adopting: Mutex::new(()),
};
let probe = reader.probe_len()?;
reader.len = probe.len;
if let Some(body) = probe.whole {
let _ = reader.whole.set(body);
}
Ok(reader)
}
pub fn url(&self) -> &str {
&self.url
}
pub fn serves_whole_body(&self) -> bool {
self.whole.get().is_some()
}
fn request(&self, method: &str) -> ureq::Request {
let mut req = self.agent.request(method, &self.url);
for (k, v) in &self.headers {
req = req.set(k, v);
}
req
}
fn with_retries<T>(&self, mut call: impl FnMut() -> Result<T, Failure>) -> std::io::Result<T> {
let mut attempt = 0;
loop {
match call() {
Ok(v) => return Ok(v),
Err(Failure::Permanent(e)) => return Err(e),
Err(Failure::Transient(e)) if attempt >= self.policy.retries => {
return Err(if attempt == 0 {
e
} else {
std::io::Error::other(format!("{e} (after {} attempts)", attempt + 1))
});
}
Err(Failure::Transient(_)) => {
std::thread::sleep(self.policy.backoff(attempt));
attempt += 1;
}
}
}
}
fn probe_len(&self) -> std::io::Result<Probe> {
let head_what = format!("HEAD {}", self.url);
let head = self.with_retries(|| {
self.request("HEAD")
.call()
.map_err(|e| classify(e, &head_what))
});
let head_err = match head {
Ok(resp) => match resp
.header("content-length")
.and_then(|v| v.trim().parse::<u64>().ok())
{
Some(len) => return Ok(Probe { len, whole: None }),
None => "no Content-Length".to_string(),
},
Err(e) => e.to_string(),
};
let what = format!("bytes 0-0 of {}", self.url);
let cap = self.policy.whole_body_cap;
let probe = self.with_retries(|| {
let resp = self
.request("GET")
.set("Range", "bytes=0-0")
.call()
.map_err(|e| classify(e, &what))?;
match resp.status() {
206 => Ok(resp
.header("content-range")
.and_then(content_range_total)
.map(|len| Probe { len, whole: None })),
200 => match read_capped(resp, cap, &what)? {
Some(body) if body.is_empty() => {
Err(Failure::Permanent(std::io::Error::other(format!(
"{} answered 200 with an empty body",
self.url
))))
}
Some(body) => Ok(Some(Probe {
len: body.len() as u64,
whole: Some(body),
})),
None => Err(Failure::Permanent(whole_body_refusal(&self.url, None, cap))),
},
s => Err(Failure::Permanent(std::io::Error::other(format!(
"status {s} for {what}"
)))),
}
})?;
probe.ok_or_else(|| {
std::io::Error::other(format!(
"could not determine the file length of {}: {head_err} on HEAD, and no \
Content-Range total on a ranged GET",
self.url
))
})
}
fn bounds(&self, offset: u64, len: u64) -> std::io::Result<()> {
if offset.checked_add(len).is_none_or(|end| end > self.len) {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"range out of bounds: {offset}+{len} past the {} bytes {} reported",
self.len, self.url
),
));
}
Ok(())
}
fn adopt_whole(&self, resp: ureq::Response, what: &str) -> Result<(), Failure> {
let _guard = self.adopting.lock().unwrap_or_else(|p| p.into_inner());
if self.whole.get().is_some() {
return Ok(());
}
let cap = self.policy.whole_body_cap;
let declared = resp
.header("content-length")
.and_then(|v| v.trim().parse::<u64>().ok());
if let Some(n) = declared {
if n > cap {
return Err(Failure::Permanent(whole_body_refusal(
&self.url,
Some(n),
cap,
)));
}
if n != self.len {
return Err(Failure::Permanent(std::io::Error::other(format!(
"the host ignored the Range header and sent {n} bytes of {}, which is not \
the {} bytes it reported for the file",
self.url, self.len
))));
}
}
let body = read_capped(resp, cap, what)?
.ok_or_else(|| Failure::Permanent(whole_body_refusal(&self.url, None, cap)))?;
if body.len() as u64 != self.len {
return Err(Failure::Permanent(std::io::Error::other(format!(
"the host ignored the Range header and sent {} bytes of {}, which is not the \
{} bytes it reported for the file",
body.len(),
self.url,
self.len
))));
}
if self.policy.trace {
eprintln!(
"[http] {} ignored Range: every read is served from its {}-byte body",
self.url,
body.len()
);
}
let _ = self.whole.set(body);
Ok(())
}
fn fetch_once(&self, offset: u64, len: u64) -> Result<Vec<u8>, Failure> {
let end = offset + len - 1; let what = format!("bytes {offset}-{end} of {}", self.url);
let resp = self
.request("GET")
.set("Range", &format!("bytes={offset}-{end}"))
.call()
.map_err(|e| classify(e, &what))?;
if resp.status() == 200 {
self.adopt_whole(resp, &what)?;
let whole = self.whole.get().expect("adopted above");
return slice_whole(whole, offset, len, &self.url).map_err(Failure::Permanent);
}
if resp.status() != 206 {
return Err(Failure::Permanent(std::io::Error::other(format!(
"server answered status {} (expected 206 Partial Content) for {what}; \
the host must support HTTP range requests",
resp.status()
))));
}
if let Some(start) = resp.header("content-range").and_then(content_range_start) {
if start != offset {
return Err(Failure::Permanent(std::io::Error::other(format!(
"server answered a Content-Range starting at {start} for a request at \
offset {offset} ({})",
self.url
))));
}
}
let size = usize::try_from(len).map_err(|_| {
Failure::Permanent(std::io::Error::other(format!(
"a {len}-byte range is too large for memory"
)))
})?;
let mut buf = Vec::with_capacity(size);
resp.into_reader()
.take(len + 1)
.read_to_end(&mut buf)
.map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
if (buf.len() as u64) < len {
return Err(Failure::Transient(std::io::Error::other(format!(
"short range response: got {} of {len} bytes at offset {offset} from {}",
buf.len(),
self.url
))));
}
buf.truncate(size);
Ok(buf)
}
fn fetch(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
if let Some(whole) = self.whole.get() {
return slice_whole(whole, offset, len, &self.url);
}
self.with_retries(|| self.fetch_once(offset, len))
}
fn fetch_spans(&self, spans: &[Span]) -> std::io::Result<Vec<Vec<u8>>> {
let workers = self.policy.concurrency.min(spans.len());
if workers <= 1 {
return spans.iter().map(|s| self.fetch(s.offset, s.len)).collect();
}
let next = AtomicUsize::new(0);
let failed = AtomicBool::new(false);
type Slot = Mutex<Option<std::io::Result<Vec<u8>>>>;
let slots: Vec<Slot> = spans.iter().map(|_| Mutex::new(None)).collect();
let panicked = std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_| {
scope.spawn(|| loop {
if failed.load(Ordering::Relaxed) {
break; }
let i = next.fetch_add(1, Ordering::Relaxed);
let Some(s) = spans.get(i) else { break };
let r = self.fetch(s.offset, s.len);
if r.is_err() {
failed.store(true, Ordering::Relaxed);
}
*slots[i].lock().unwrap_or_else(|p| p.into_inner()) = Some(r);
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().is_err())
.fold(false, |a, b| a | b)
});
if panicked {
return Err(std::io::Error::other("read_many worker panicked"));
}
let mut out = Vec::with_capacity(spans.len());
let mut first_err = None;
for slot in slots {
match slot.into_inner().unwrap_or_else(|p| p.into_inner()) {
Some(Ok(b)) => out.push(b),
Some(Err(e)) => {
first_err.get_or_insert(e);
}
None => {} }
}
match first_err {
Some(e) => Err(e),
None if out.len() == spans.len() => Ok(out),
None => Err(std::io::Error::other("read_many left a range unfetched")),
}
}
}
impl RangeReader for HttpRangeReader {
fn len(&self) -> u64 {
self.len
}
fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
if len == 0 {
return Ok(Vec::new());
}
self.bounds(offset, len)?;
if self.policy.trace && self.whole.get().is_none() {
eprintln!("[http] read {offset}+{len}");
}
self.fetch(offset, len)
}
fn concurrency(&self) -> usize {
self.policy.concurrency
}
fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
for &(o, l) in ranges {
if l > 0 {
self.bounds(o, l)?;
}
}
if let Some(whole) = self.whole.get() {
return ranges
.iter()
.map(|&(o, l)| slice_whole(whole, o, l, &self.url))
.collect();
}
let (spans, place) = coalesce(ranges, self.policy.coalesce_gap, self.policy.max_span)?;
if self.policy.trace {
let bytes: u64 = spans.iter().map(|s| s.len).sum();
eprintln!(
"[http] batch of {} range(s) -> {} request(s), {bytes} bytes, {} in flight",
ranges.len(),
spans.len(),
self.policy.concurrency.min(spans.len())
);
}
let blobs = self.fetch_spans(&spans)?;
Ok(place
.into_iter()
.zip(ranges)
.map(|(p, &(_, len))| match p {
None => Vec::new(),
Some((si, at)) => {
let at = at as usize;
blobs[si][at..at + len as usize].to_vec()
}
})
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::net::TcpListener;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
#[derive(Clone, Copy)]
enum ServerMode {
HonorRange,
IgnoreRange,
IgnoreRangeUnsized,
HeadWithoutLength,
HeadWithoutLengthIgnoreRange,
TruncateBody,
Error500,
NotFound,
Unavailable(u64),
WrongOffset,
FailAt(usize),
RequireHeader,
}
#[derive(Default)]
struct Stats {
gets: AtomicU64,
in_flight: AtomicU64,
max_in_flight: AtomicU64,
}
fn serve_with(data: Vec<u8>, mode: ServerMode, delay: Duration) -> (String, Arc<Stats>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}/", listener.local_addr().unwrap());
let stats = Arc::new(Stats::default());
let data = Arc::new(data);
let st = stats.clone();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let (data, st) = (data.clone(), st.clone());
std::thread::spawn(move || handle(stream, &data, mode, delay, &st));
}
});
(url, stats)
}
fn serve(data: Vec<u8>, mode: ServerMode) -> String {
serve_with(data, mode, Duration::ZERO).0
}
fn handle(
mut stream: std::net::TcpStream,
data: &[u8],
mode: ServerMode,
delay: Duration,
st: &Stats,
) {
let mut req = Vec::new();
let mut byte = [0u8; 1];
while !req.ends_with(b"\r\n\r\n") {
use std::io::Read as _;
if stream.read(&mut byte).unwrap_or(0) == 0 {
break;
}
req.push(byte[0]);
}
let text = String::from_utf8_lossy(&req);
let is_head = text.starts_with("HEAD");
let range = text.lines().find_map(|l| {
let l = l.to_ascii_lowercase();
l.strip_prefix("range: bytes=")
.map(|r| r.trim().to_string())
});
let authorized = text
.lines()
.any(|l| l.eq_ignore_ascii_case("x-token: secret"));
let total = data.len();
let status_only = |stream: &mut std::net::TcpStream, line: &str| {
let _ = stream.write_all(
format!("HTTP/1.1 {line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
.as_bytes(),
);
};
if matches!(mode, ServerMode::RequireHeader) && !authorized {
status_only(&mut stream, "401 Unauthorized");
return;
}
if is_head {
let h = if matches!(
mode,
ServerMode::HeadWithoutLength | ServerMode::HeadWithoutLengthIgnoreRange
) {
"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n".to_string()
} else {
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nAccept-Ranges: bytes\r\nConnection: close\r\n\r\n"
)
};
let _ = stream.write_all(h.as_bytes());
return;
}
let n = st.gets.fetch_add(1, Ordering::SeqCst) + 1;
let now = st.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
st.max_in_flight.fetch_max(now, Ordering::SeqCst);
std::thread::sleep(delay);
st.in_flight.fetch_sub(1, Ordering::SeqCst);
let ignores_range = matches!(
mode,
ServerMode::IgnoreRange
| ServerMode::IgnoreRangeUnsized
| ServerMode::HeadWithoutLengthIgnoreRange
);
match (mode, range) {
(ServerMode::Error500, _) => status_only(&mut stream, "500 Internal Server Error"),
(ServerMode::NotFound, _) => status_only(&mut stream, "404 Not Found"),
(ServerMode::Unavailable(k), _) if n <= k => {
status_only(&mut stream, "503 Service Unavailable")
}
(ServerMode::FailAt(at), Some(r)) if r.starts_with(&format!("{at}-")) => {
status_only(&mut stream, "404 Not Found")
}
(ServerMode::IgnoreRangeUnsized, _) => {
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
let _ = stream.write_all(data);
}
(_, Some(r)) if !ignores_range => {
let (a, b) = r.split_once('-').unwrap();
let a: usize = a.parse().unwrap();
let b: usize = b.parse().unwrap();
let slice = &data[a.min(total)..=b.min(total - 1)];
let claimed = if matches!(mode, ServerMode::WrongOffset) {
a + 1
} else {
a
};
let h = format!(
"HTTP/1.1 206 Partial Content\r\nContent-Range: bytes {claimed}-{b}/{total}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
slice.len()
);
let _ = stream.write_all(h.as_bytes());
let sent = if matches!(mode, ServerMode::TruncateBody) {
&slice[..slice.len() / 2] } else {
slice
};
let _ = stream.write_all(sent);
}
_ => {
let h = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nConnection: close\r\n\r\n"
);
let _ = stream.write_all(h.as_bytes());
let _ = stream.write_all(data);
}
}
}
fn quick() -> FetchPolicy {
FetchPolicy {
backoff_ms: 1,
..FetchPolicy::DEFAULT
}
}
#[test]
fn reads_exact_ranges_from_a_range_host() {
let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
let url = serve(data.clone(), ServerMode::HonorRange);
let r = HttpRangeReader::open(&url).unwrap();
assert_eq!(r.len(), 1000);
assert_eq!(r.read_at(0, 4).unwrap(), &data[0..4]);
assert_eq!(r.read_at(500, 20).unwrap(), &data[500..520]);
assert_eq!(r.read_at(996, 4).unwrap(), &data[996..1000]);
assert!(r.read_at(0, 0).unwrap().is_empty());
assert!(!r.serves_whole_body());
}
#[test]
#[ignore = "hits the network (httpbin); run with --ignored"]
fn reads_ranges_over_https() {
let url = "https://httpbin.org/range/2048";
let r = HttpRangeReader::open(url).unwrap();
assert!(r.len() >= 2048);
let chunk = r.read_at(100, 16).unwrap();
assert_eq!(chunk.len(), 16);
}
#[test]
fn keeps_the_whole_body_of_a_host_that_ignores_range() {
let data: Vec<u8> = (0..=255u8).cycle().take(5000).collect();
let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRange, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
assert_eq!(r.len(), 5000);
assert_eq!(r.read_at(50, 10).unwrap(), &data[50..60]);
assert!(r.serves_whole_body());
assert_eq!(r.read_at(4990, 10).unwrap(), &data[4990..5000]);
let got = r.read_many(&[(0, 4), (3000, 100), (7, 0)]).unwrap();
assert_eq!(got[0], &data[0..4]);
assert_eq!(got[1], &data[3000..3100]);
assert!(got[2].is_empty());
assert_eq!(
stats.gets.load(Ordering::SeqCst),
1,
"one download, then memory"
);
assert!(r.read_at(4999, 2).is_err());
}
#[test]
fn keeps_an_unsized_whole_body_too() {
let data: Vec<u8> = (0..=255u8).cycle().take(3000).collect();
let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRangeUnsized, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
assert_eq!(
r.read_many(&[(10, 5), (2000, 5)]).unwrap()[1],
&data[2000..2005]
);
assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
}
#[test]
fn refuses_a_whole_body_over_the_cap() {
let small_cap = FetchPolicy {
whole_body_cap: 1000,
..quick()
};
let data: Vec<u8> = (0..=255u8).cycle().take(4000).collect();
for mode in [ServerMode::IgnoreRange, ServerMode::IgnoreRangeUnsized] {
let (url, stats) = serve_with(data.clone(), mode, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, small_cap).unwrap();
let err = r.read_at(50, 10).unwrap_err().to_string();
assert!(
err.contains("ignored the Range header") && err.contains("not kept in memory"),
"{err}"
);
assert_eq!(stats.gets.load(Ordering::SeqCst), 1, "permanent: no retry");
assert!(!r.serves_whole_body());
assert!(r.read_many(&[(0, 1), (2000, 1)]).is_err());
}
assert_eq!(FetchPolicy::DEFAULT.whole_body_cap, 256 << 20);
let msg = whole_body_refusal("u", Some(300 << 20), WHOLE_BODY_CAP).to_string();
assert!(
msg.contains("256 MiB") && msg.contains("status 200"),
"{msg}"
);
}
#[test]
fn measures_the_length_without_a_head_content_length() {
let data: Vec<u8> = (0..=255u8).cycle().take(2048).collect();
let (url, stats) = serve_with(data.clone(), ServerMode::HeadWithoutLength, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
assert_eq!(r.len(), 2048);
assert_eq!(r.read_at(100, 8).unwrap(), &data[100..108]);
assert!(!r.serves_whole_body());
assert_eq!(stats.gets.load(Ordering::SeqCst), 2, "probe + read");
let (url, stats) = serve_with(
data.clone(),
ServerMode::HeadWithoutLengthIgnoreRange,
Duration::ZERO,
);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
assert_eq!(r.len(), 2048);
assert!(r.serves_whole_body());
assert_eq!(r.read_at(2000, 48).unwrap(), &data[2000..2048]);
assert_eq!(
stats.gets.load(Ordering::SeqCst),
1,
"the probe was the file"
);
}
#[test]
fn sends_the_extra_headers_on_every_request() {
let data: Vec<u8> = (0..100u8).collect();
let url = serve(data.clone(), ServerMode::RequireHeader);
assert!(HttpRangeReader::open_with(&url, quick()).is_err());
let headers = vec![("x-token".to_string(), "secret".to_string())];
let r = HttpRangeReader::open_with_headers(&url, headers, quick()).unwrap();
assert_eq!(r.len(), 100);
assert_eq!(r.read_many(&[(0, 2), (90, 10)]).unwrap()[1], &data[90..100]);
}
#[test]
fn rejects_a_truncated_range_response() {
let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
let (url, stats) = serve_with(data, ServerMode::TruncateBody, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
let err = r.read_at(100, 40).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("short range response") || msg.contains("closed before"),
"expected a truncation error, got: {err}"
);
assert_eq!(
stats.gets.load(Ordering::SeqCst),
3,
"a truncated body is transient: retried twice"
);
let data: Vec<u8> = (0..100u8).collect();
let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
let err = r.read_at(90, 20).unwrap_err();
assert!(err.to_string().contains("out of bounds"), "{err}");
assert!(r.read_many(&[(0, 1), (90, 20)]).is_err());
assert_eq!(stats.gets.load(Ordering::SeqCst), 0);
}
#[test]
fn server_errors_are_clean_errors() {
let data: Vec<u8> = (0..100u8).collect();
let (url, stats) = serve_with(data, ServerMode::Error500, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
let err = r.read_at(0, 10).unwrap_err();
assert!(err.to_string().contains("HTTP 500"), "{err}");
assert!(err.to_string().contains("after 3 attempts"), "{err}");
assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
}
#[test]
fn a_missing_file_is_not_retried() {
let (url, stats) = serve_with(vec![0; 100], ServerMode::NotFound, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
let err = r.read_at(0, 10).unwrap_err();
assert!(err.to_string().contains("HTTP 404"), "{err}");
assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
}
#[test]
fn a_transient_failure_is_retried_and_recovers() {
let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
let (url, stats) = serve_with(data.clone(), ServerMode::Unavailable(2), Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
assert_eq!(r.read_at(10, 30).unwrap(), &data[10..40]);
assert_eq!(stats.gets.load(Ordering::SeqCst), 3, "two 503s, then 206");
let (url, _) = serve_with(data, ServerMode::Unavailable(1), Duration::ZERO);
let none = FetchPolicy {
retries: 0,
..quick()
};
let r = HttpRangeReader::open_with(&url, none).unwrap();
assert!(r.read_at(10, 30).unwrap_err().to_string().contains("503"));
}
#[test]
fn rejects_a_range_answered_for_the_wrong_offset() {
let (url, _) = serve_with(vec![7; 1000], ServerMode::WrongOffset, Duration::ZERO);
let r = HttpRangeReader::open_with(&url, quick()).unwrap();
let err = r.read_at(100, 10).unwrap_err();
assert!(
err.to_string().contains("Content-Range starting at 101"),
"{err}"
);
}
#[test]
fn read_many_overlaps_requests_up_to_the_bound() {
let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
let (url, stats) = serve_with(
data.clone(),
ServerMode::HonorRange,
Duration::from_millis(40),
);
let policy = FetchPolicy {
concurrency: 4,
coalesce_gap: 0,
..quick()
};
let r = HttpRangeReader::open_with(&url, policy).unwrap();
let mut ranges: Vec<(u64, u64)> = (0..12).map(|i| (i * 65_536 + 7, 100)).collect();
ranges.reverse();
ranges.insert(3, (5, 0));
let t = std::time::Instant::now();
let got = r.read_many(&ranges).unwrap();
let took = t.elapsed();
assert_eq!(got.len(), ranges.len());
for (&(o, l), b) in ranges.iter().zip(&got) {
assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
}
assert_eq!(stats.gets.load(Ordering::SeqCst), 12);
assert_eq!(
stats.max_in_flight.load(Ordering::SeqCst),
4,
"bounded fan-out"
);
assert!(took < Duration::from_millis(12 * 40), "{took:?}");
}
#[test]
fn concurrency_one_is_sequential() {
let data: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::from_millis(5));
let policy = FetchPolicy {
concurrency: 1,
coalesce_gap: 0,
..quick()
};
let r = HttpRangeReader::open_with(&url, policy).unwrap();
assert_eq!(r.concurrency(), 1);
r.read_many(&[(0, 10), (1000, 10), (3000, 10)]).unwrap();
assert_eq!(stats.max_in_flight.load(Ordering::SeqCst), 1);
assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
}
#[test]
fn read_many_coalesces_nearby_ranges() {
let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
let (url, stats) = serve_with(data.clone(), ServerMode::HonorRange, Duration::ZERO);
let policy = FetchPolicy {
coalesce_gap: 1024,
..quick()
};
let r = HttpRangeReader::open_with(&url, policy).unwrap();
let ranges = [(5000, 100), (100, 50), (200, 10), (190, 30), (6000, 1)];
let got = r.read_many(&ranges).unwrap();
for (&(o, l), b) in ranges.iter().zip(&got) {
assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
}
assert_eq!(stats.gets.load(Ordering::SeqCst), 2);
}
#[test]
fn read_many_fails_the_batch_on_one_bad_range() {
let data: Vec<u8> = (0..100u8).collect();
let url = serve(data, ServerMode::FailAt(50));
let r = HttpRangeReader::open_with(
&url,
FetchPolicy {
coalesce_gap: 0,
..quick()
},
)
.unwrap();
let err = r.read_many(&[(0, 10), (50, 10), (20, 10)]).unwrap_err();
assert!(
err.to_string().contains("HTTP 404 for bytes 50-59"),
"{err}"
);
assert!(r.read_many(&[(0, 10), (20, 10)]).is_ok());
}
#[test]
fn coalesce_plans_spans_and_placements() {
let (spans, place) = coalesce(&[(10, 5), (0, 0), (18, 4), (100, 1)], 3, 1 << 20).unwrap();
assert_eq!(
spans,
vec![
Span {
offset: 10,
len: 12
},
Span {
offset: 100,
len: 1
}
]
);
assert_eq!(place, vec![Some((0, 0)), None, Some((0, 8)), Some((1, 0))]);
let (spans, _) = coalesce(&[(0, 10), (10, 10), (20, 10)], 0, 20).unwrap();
assert_eq!(spans.len(), 2);
assert!(coalesce(&[(u64::MAX, 2)], 0, 10).is_err());
}
#[test]
fn parses_content_range() {
assert_eq!(content_range_start("bytes 100-199/1000"), Some(100));
assert_eq!(content_range_start(" bytes 0-0/*"), Some(0));
assert_eq!(content_range_start("items 1-2/3"), None);
assert_eq!(content_range_total("bytes 0-0/1000"), Some(1000));
assert_eq!(content_range_total("bytes 0-0/*"), None);
}
#[test]
fn retryable_statuses() {
for c in [408, 429, 500, 502, 503, 504] {
assert!(retryable_status(c), "{c}");
}
for c in [400, 403, 404, 416, 501, 505] {
assert!(!retryable_status(c), "{c}");
}
}
}