1use std::io::Read;
44use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
45use std::sync::{Mutex, OnceLock};
46use std::time::Duration;
47
48use crate::RangeReader;
49
50pub const WHOLE_BODY_CAP: u64 = 256 << 20;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct FetchPolicy {
58 pub concurrency: usize,
62 pub coalesce_gap: u64,
66 pub max_span: u64,
69 pub retries: u32,
72 pub backoff_ms: u64,
74 pub trace: bool,
77 pub whole_body_cap: u64,
80}
81
82impl FetchPolicy {
83 pub const DEFAULT: FetchPolicy = FetchPolicy {
87 concurrency: 16,
88 coalesce_gap: 32 << 10,
89 max_span: 8 << 20,
90 retries: 2,
91 backoff_ms: 100,
92 trace: false,
93 whole_body_cap: WHOLE_BODY_CAP,
94 };
95
96 pub fn from_env() -> Self {
98 let num = |k: &str| {
99 std::env::var(k)
100 .ok()
101 .and_then(|v| v.trim().parse::<u64>().ok())
102 };
103 let d = Self::DEFAULT;
104 FetchPolicy {
105 concurrency: num("RETE_HTTP_CONCURRENCY")
106 .map_or(d.concurrency, |n| n.clamp(1, 64) as usize),
107 coalesce_gap: num("RETE_HTTP_COALESCE_KB")
108 .map_or(d.coalesce_gap, |kb| kb.saturating_mul(1024)),
109 max_span: d.max_span,
110 retries: num("RETE_HTTP_RETRIES").map_or(d.retries, |n| n.min(10) as u32),
111 backoff_ms: d.backoff_ms,
112 trace: std::env::var_os("RETE_HTTP_TRACE").is_some(),
113 whole_body_cap: d.whole_body_cap,
114 }
115 }
116
117 fn backoff(&self, attempt: u32) -> Duration {
118 let ms = self
119 .backoff_ms
120 .saturating_mul(4u64.saturating_pow(attempt))
121 .min(2000);
122 Duration::from_millis(ms)
123 }
124}
125
126impl Default for FetchPolicy {
127 fn default() -> Self {
128 Self::DEFAULT
129 }
130}
131
132enum Failure {
134 Transient(std::io::Error),
135 Permanent(std::io::Error),
136}
137
138fn retryable_status(code: u16) -> bool {
141 code == 408 || code == 429 || ((500..600).contains(&code) && code != 501 && code != 505)
142}
143
144fn classify(err: ureq::Error, what: &str) -> Failure {
145 match err {
146 ureq::Error::Status(code, _) => {
147 let e = std::io::Error::other(format!("HTTP {code} for {what}"));
148 if retryable_status(code) {
149 Failure::Transient(e)
150 } else {
151 Failure::Permanent(e)
152 }
153 }
154 ureq::Error::Transport(t) => {
155 let transient = matches!(
156 t.kind(),
157 ureq::ErrorKind::Io
158 | ureq::ErrorKind::ConnectionFailed
159 | ureq::ErrorKind::Dns
160 | ureq::ErrorKind::ProxyConnect
161 );
162 let e = std::io::Error::other(format!("{t} ({what})"));
163 if transient {
164 Failure::Transient(e)
165 } else {
166 Failure::Permanent(e)
167 }
168 }
169 }
170}
171
172fn content_range_start(v: &str) -> Option<u64> {
174 let v = v.trim().strip_prefix("bytes")?.trim_start();
175 v.split(['-', '/']).next()?.trim().parse().ok()
176}
177
178fn content_range_total(v: &str) -> Option<u64> {
180 v.rsplit('/').next()?.trim().parse().ok()
181}
182
183#[derive(Debug, PartialEq, Eq)]
185struct Span {
186 offset: u64,
187 len: u64,
188}
189
190type Placement = Vec<Option<(usize, u64)>>;
193
194fn coalesce(
199 ranges: &[(u64, u64)],
200 gap: u64,
201 max_span: u64,
202) -> std::io::Result<(Vec<Span>, Placement)> {
203 let mut order: Vec<usize> = (0..ranges.len()).filter(|&i| ranges[i].1 > 0).collect();
204 for &i in &order {
205 let (o, l) = ranges[i];
206 if o.checked_add(l).is_none() {
207 return Err(std::io::Error::new(
208 std::io::ErrorKind::InvalidInput,
209 format!("range {o}+{l} overflows"),
210 ));
211 }
212 }
213 order.sort_by_key(|&i| ranges[i]);
214 let mut spans: Vec<Span> = Vec::new();
215 let mut place: Placement = vec![None; ranges.len()];
216 for i in order {
217 let (o, l) = ranges[i];
218 let end = o + l;
219 let joins = spans.last().is_some_and(|s| {
220 let s_end = s.offset + s.len;
221 o <= s_end.saturating_add(gap) && end.max(s_end) - s.offset <= max_span
222 });
223 if joins {
224 let s = spans.last_mut().expect("checked above");
225 s.len = (s.offset + s.len).max(end) - s.offset;
226 } else {
227 spans.push(Span { offset: o, len: l });
228 }
229 let si = spans.len() - 1;
230 place[i] = Some((si, o - spans[si].offset));
231 }
232 Ok((spans, place))
233}
234
235fn whole_body_refusal(url: &str, bytes: Option<u64>, cap: u64) -> std::io::Error {
238 let size = match bytes {
239 Some(n) => format!("all {n} bytes"),
240 None => format!("more than {cap} bytes"),
241 };
242 std::io::Error::other(format!(
243 "the host ignored the Range header and sent {size} of {url} (status 200 instead \
244 of 206 Partial Content); a full body over {} MiB is not kept in memory — host the \
245 file where HTTP range requests work",
246 cap >> 20
247 ))
248}
249
250fn slice_whole(whole: &[u8], offset: u64, len: u64, url: &str) -> std::io::Result<Vec<u8>> {
253 let end = offset
254 .checked_add(len)
255 .filter(|&end| end <= whole.len() as u64)
256 .ok_or_else(|| {
257 std::io::Error::other(format!(
258 "range {offset}+{len} is past the end of the {} bytes {url} sent",
259 whole.len()
260 ))
261 })?;
262 Ok(whole[offset as usize..end as usize].to_vec())
263}
264
265fn read_capped(resp: ureq::Response, cap: u64, what: &str) -> Result<Option<Vec<u8>>, Failure> {
268 let declared = resp
269 .header("content-length")
270 .and_then(|v| v.trim().parse::<u64>().ok());
271 if declared.is_some_and(|n| n > cap) {
272 return Ok(None); }
274 let mut buf = Vec::with_capacity(declared.unwrap_or(0).min(cap) as usize);
275 resp.into_reader()
276 .take(cap.saturating_add(1))
277 .read_to_end(&mut buf)
278 .map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
279 if buf.len() as u64 > cap {
280 return Ok(None);
281 }
282 if declared.is_some_and(|n| n != buf.len() as u64) {
283 return Err(Failure::Transient(std::io::Error::other(format!(
284 "short body: got {} of the {} bytes {what} declared",
285 buf.len(),
286 declared.unwrap_or(0)
287 ))));
288 }
289 Ok(Some(buf))
290}
291
292struct Probe {
295 len: u64,
296 whole: Option<Vec<u8>>,
297}
298
299pub struct HttpRangeReader {
303 url: String,
304 len: u64,
305 agent: ureq::Agent,
306 policy: FetchPolicy,
307 headers: Vec<(String, String)>,
309 whole: OnceLock<Vec<u8>>,
312 adopting: Mutex<()>,
315}
316
317impl HttpRangeReader {
318 pub fn open(url: &str) -> std::io::Result<Self> {
320 Self::open_with(url, FetchPolicy::from_env())
321 }
322
323 pub fn open_with(url: &str, policy: FetchPolicy) -> std::io::Result<Self> {
325 Self::open_with_headers(url, Vec::new(), policy)
326 }
327
328 pub fn open_with_headers(
334 url: &str,
335 headers: Vec<(String, String)>,
336 policy: FetchPolicy,
337 ) -> std::io::Result<Self> {
338 let agent = ureq::AgentBuilder::new()
339 .max_idle_connections(policy.concurrency.max(4) * 2)
341 .max_idle_connections_per_host(policy.concurrency)
342 .timeout_connect(Duration::from_secs(30))
343 .timeout_read(Duration::from_secs(120))
344 .build();
345 let mut reader = HttpRangeReader {
346 url: url.to_string(),
347 len: 0,
348 agent,
349 policy,
350 headers,
351 whole: OnceLock::new(),
352 adopting: Mutex::new(()),
353 };
354 let probe = reader.probe_len()?;
355 reader.len = probe.len;
356 if let Some(body) = probe.whole {
357 let _ = reader.whole.set(body);
358 }
359 Ok(reader)
360 }
361
362 pub fn url(&self) -> &str {
364 &self.url
365 }
366
367 pub fn serves_whole_body(&self) -> bool {
370 self.whole.get().is_some()
371 }
372
373 fn request(&self, method: &str) -> ureq::Request {
374 let mut req = self.agent.request(method, &self.url);
375 for (k, v) in &self.headers {
376 req = req.set(k, v);
377 }
378 req
379 }
380
381 fn with_retries<T>(&self, mut call: impl FnMut() -> Result<T, Failure>) -> std::io::Result<T> {
383 let mut attempt = 0;
384 loop {
385 match call() {
386 Ok(v) => return Ok(v),
387 Err(Failure::Permanent(e)) => return Err(e),
388 Err(Failure::Transient(e)) if attempt >= self.policy.retries => {
389 return Err(if attempt == 0 {
390 e
391 } else {
392 std::io::Error::other(format!("{e} (after {} attempts)", attempt + 1))
393 });
394 }
395 Err(Failure::Transient(_)) => {
396 std::thread::sleep(self.policy.backoff(attempt));
397 attempt += 1;
398 }
399 }
400 }
401 }
402
403 fn probe_len(&self) -> std::io::Result<Probe> {
407 let head_what = format!("HEAD {}", self.url);
408 let head = self.with_retries(|| {
409 self.request("HEAD")
410 .call()
411 .map_err(|e| classify(e, &head_what))
412 });
413 let head_err = match head {
414 Ok(resp) => match resp
415 .header("content-length")
416 .and_then(|v| v.trim().parse::<u64>().ok())
417 {
418 Some(len) => return Ok(Probe { len, whole: None }),
419 None => "no Content-Length".to_string(),
420 },
421 Err(e) => e.to_string(),
422 };
423 let what = format!("bytes 0-0 of {}", self.url);
424 let cap = self.policy.whole_body_cap;
425 let probe = self.with_retries(|| {
426 let resp = self
427 .request("GET")
428 .set("Range", "bytes=0-0")
429 .call()
430 .map_err(|e| classify(e, &what))?;
431 match resp.status() {
432 206 => Ok(resp
433 .header("content-range")
434 .and_then(content_range_total)
435 .map(|len| Probe { len, whole: None })),
436 200 => match read_capped(resp, cap, &what)? {
437 Some(body) if body.is_empty() => {
438 Err(Failure::Permanent(std::io::Error::other(format!(
439 "{} answered 200 with an empty body",
440 self.url
441 ))))
442 }
443 Some(body) => Ok(Some(Probe {
444 len: body.len() as u64,
445 whole: Some(body),
446 })),
447 None => Err(Failure::Permanent(whole_body_refusal(&self.url, None, cap))),
448 },
449 s => Err(Failure::Permanent(std::io::Error::other(format!(
450 "status {s} for {what}"
451 )))),
452 }
453 })?;
454 probe.ok_or_else(|| {
455 std::io::Error::other(format!(
456 "could not determine the file length of {}: {head_err} on HEAD, and no \
457 Content-Range total on a ranged GET",
458 self.url
459 ))
460 })
461 }
462
463 fn bounds(&self, offset: u64, len: u64) -> std::io::Result<()> {
466 if offset.checked_add(len).is_none_or(|end| end > self.len) {
467 return Err(std::io::Error::new(
468 std::io::ErrorKind::UnexpectedEof,
469 format!(
470 "range out of bounds: {offset}+{len} past the {} bytes {} reported",
471 self.len, self.url
472 ),
473 ));
474 }
475 Ok(())
476 }
477
478 fn adopt_whole(&self, resp: ureq::Response, what: &str) -> Result<(), Failure> {
482 let _guard = self.adopting.lock().unwrap_or_else(|p| p.into_inner());
483 if self.whole.get().is_some() {
484 return Ok(());
485 }
486 let cap = self.policy.whole_body_cap;
487 let declared = resp
488 .header("content-length")
489 .and_then(|v| v.trim().parse::<u64>().ok());
490 if let Some(n) = declared {
491 if n > cap {
492 return Err(Failure::Permanent(whole_body_refusal(
493 &self.url,
494 Some(n),
495 cap,
496 )));
497 }
498 if n != self.len {
499 return Err(Failure::Permanent(std::io::Error::other(format!(
500 "the host ignored the Range header and sent {n} bytes of {}, which is not \
501 the {} bytes it reported for the file",
502 self.url, self.len
503 ))));
504 }
505 }
506 let body = read_capped(resp, cap, what)?
507 .ok_or_else(|| Failure::Permanent(whole_body_refusal(&self.url, None, cap)))?;
508 if body.len() as u64 != self.len {
509 return Err(Failure::Permanent(std::io::Error::other(format!(
510 "the host ignored the Range header and sent {} bytes of {}, which is not the \
511 {} bytes it reported for the file",
512 body.len(),
513 self.url,
514 self.len
515 ))));
516 }
517 if self.policy.trace {
518 eprintln!(
519 "[http] {} ignored Range: every read is served from its {}-byte body",
520 self.url,
521 body.len()
522 );
523 }
524 let _ = self.whole.set(body);
525 Ok(())
526 }
527
528 fn fetch_once(&self, offset: u64, len: u64) -> Result<Vec<u8>, Failure> {
530 let end = offset + len - 1; let what = format!("bytes {offset}-{end} of {}", self.url);
532 let resp = self
533 .request("GET")
534 .set("Range", &format!("bytes={offset}-{end}"))
535 .call()
536 .map_err(|e| classify(e, &what))?;
537 if resp.status() == 200 {
541 self.adopt_whole(resp, &what)?;
542 let whole = self.whole.get().expect("adopted above");
543 return slice_whole(whole, offset, len, &self.url).map_err(Failure::Permanent);
544 }
545 if resp.status() != 206 {
546 return Err(Failure::Permanent(std::io::Error::other(format!(
547 "server answered status {} (expected 206 Partial Content) for {what}; \
548 the host must support HTTP range requests",
549 resp.status()
550 ))));
551 }
552 if let Some(start) = resp.header("content-range").and_then(content_range_start) {
554 if start != offset {
555 return Err(Failure::Permanent(std::io::Error::other(format!(
556 "server answered a Content-Range starting at {start} for a request at \
557 offset {offset} ({})",
558 self.url
559 ))));
560 }
561 }
562 let size = usize::try_from(len).map_err(|_| {
563 Failure::Permanent(std::io::Error::other(format!(
564 "a {len}-byte range is too large for memory"
565 )))
566 })?;
567 let mut buf = Vec::with_capacity(size);
568 resp.into_reader()
571 .take(len + 1)
572 .read_to_end(&mut buf)
573 .map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
574 if (buf.len() as u64) < len {
578 return Err(Failure::Transient(std::io::Error::other(format!(
579 "short range response: got {} of {len} bytes at offset {offset} from {}",
580 buf.len(),
581 self.url
582 ))));
583 }
584 buf.truncate(size);
585 Ok(buf)
586 }
587
588 fn fetch(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
591 if let Some(whole) = self.whole.get() {
592 return slice_whole(whole, offset, len, &self.url);
593 }
594 self.with_retries(|| self.fetch_once(offset, len))
595 }
596
597 fn fetch_spans(&self, spans: &[Span]) -> std::io::Result<Vec<Vec<u8>>> {
599 let workers = self.policy.concurrency.min(spans.len());
600 if workers <= 1 {
601 return spans.iter().map(|s| self.fetch(s.offset, s.len)).collect();
602 }
603 let next = AtomicUsize::new(0);
604 let failed = AtomicBool::new(false);
605 type Slot = Mutex<Option<std::io::Result<Vec<u8>>>>;
606 let slots: Vec<Slot> = spans.iter().map(|_| Mutex::new(None)).collect();
607 let panicked = std::thread::scope(|scope| {
608 let handles: Vec<_> = (0..workers)
609 .map(|_| {
610 scope.spawn(|| loop {
611 if failed.load(Ordering::Relaxed) {
612 break; }
614 let i = next.fetch_add(1, Ordering::Relaxed);
615 let Some(s) = spans.get(i) else { break };
616 let r = self.fetch(s.offset, s.len);
617 if r.is_err() {
618 failed.store(true, Ordering::Relaxed);
619 }
620 *slots[i].lock().unwrap_or_else(|p| p.into_inner()) = Some(r);
621 })
622 })
623 .collect();
624 handles
625 .into_iter()
626 .map(|h| h.join().is_err())
627 .fold(false, |a, b| a | b)
628 });
629 if panicked {
630 return Err(std::io::Error::other("read_many worker panicked"));
631 }
632 let mut out = Vec::with_capacity(spans.len());
633 let mut first_err = None;
634 for slot in slots {
635 match slot.into_inner().unwrap_or_else(|p| p.into_inner()) {
636 Some(Ok(b)) => out.push(b),
637 Some(Err(e)) => {
638 first_err.get_or_insert(e);
639 }
640 None => {} }
642 }
643 match first_err {
644 Some(e) => Err(e),
645 None if out.len() == spans.len() => Ok(out),
646 None => Err(std::io::Error::other("read_many left a range unfetched")),
647 }
648 }
649}
650
651impl RangeReader for HttpRangeReader {
652 fn len(&self) -> u64 {
653 self.len
654 }
655
656 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
657 if len == 0 {
658 return Ok(Vec::new());
659 }
660 self.bounds(offset, len)?;
661 if self.policy.trace && self.whole.get().is_none() {
662 eprintln!("[http] read {offset}+{len}");
663 }
664 self.fetch(offset, len)
665 }
666
667 fn concurrency(&self) -> usize {
670 self.policy.concurrency
671 }
672
673 fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
676 for &(o, l) in ranges {
677 if l > 0 {
678 self.bounds(o, l)?;
679 }
680 }
681 if let Some(whole) = self.whole.get() {
682 return ranges
683 .iter()
684 .map(|&(o, l)| slice_whole(whole, o, l, &self.url))
685 .collect();
686 }
687 let (spans, place) = coalesce(ranges, self.policy.coalesce_gap, self.policy.max_span)?;
688 if self.policy.trace {
689 let bytes: u64 = spans.iter().map(|s| s.len).sum();
690 eprintln!(
691 "[http] batch of {} range(s) -> {} request(s), {bytes} bytes, {} in flight",
692 ranges.len(),
693 spans.len(),
694 self.policy.concurrency.min(spans.len())
695 );
696 }
697 let blobs = self.fetch_spans(&spans)?;
698 Ok(place
699 .into_iter()
700 .zip(ranges)
701 .map(|(p, &(_, len))| match p {
702 None => Vec::new(),
703 Some((si, at)) => {
704 let at = at as usize;
705 blobs[si][at..at + len as usize].to_vec()
706 }
707 })
708 .collect())
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use std::io::Write;
716 use std::net::TcpListener;
717 use std::sync::atomic::AtomicU64;
718 use std::sync::Arc;
719
720 #[derive(Clone, Copy)]
722 enum ServerMode {
723 HonorRange,
725 IgnoreRange,
727 IgnoreRangeUnsized,
730 HeadWithoutLength,
733 HeadWithoutLengthIgnoreRange,
736 TruncateBody,
739 Error500,
741 NotFound,
743 Unavailable(u64),
745 WrongOffset,
747 FailAt(usize),
749 RequireHeader,
752 }
753
754 #[derive(Default)]
764 struct Stats {
765 gets: AtomicU64,
766 in_flight: AtomicU64,
767 max_in_flight: AtomicU64,
768 }
769
770 fn serve_with(data: Vec<u8>, mode: ServerMode, delay: Duration) -> (String, Arc<Stats>) {
775 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
776 let url = format!("http://{}/", listener.local_addr().unwrap());
777 let stats = Arc::new(Stats::default());
778 let data = Arc::new(data);
779 let st = stats.clone();
780 std::thread::spawn(move || {
781 for stream in listener.incoming() {
782 let Ok(stream) = stream else { continue };
783 let (data, st) = (data.clone(), st.clone());
784 std::thread::spawn(move || handle(stream, &data, mode, delay, &st));
785 }
786 });
787 (url, stats)
788 }
789
790 fn serve(data: Vec<u8>, mode: ServerMode) -> String {
791 serve_with(data, mode, Duration::ZERO).0
792 }
793
794 fn handle(
795 mut stream: std::net::TcpStream,
796 data: &[u8],
797 mode: ServerMode,
798 delay: Duration,
799 st: &Stats,
800 ) {
801 let mut req = Vec::new();
803 let mut byte = [0u8; 1];
804 while !req.ends_with(b"\r\n\r\n") {
805 use std::io::Read as _;
806 if stream.read(&mut byte).unwrap_or(0) == 0 {
807 break;
808 }
809 req.push(byte[0]);
810 }
811 let text = String::from_utf8_lossy(&req);
812 let is_head = text.starts_with("HEAD");
813 let range = text.lines().find_map(|l| {
814 let l = l.to_ascii_lowercase();
815 l.strip_prefix("range: bytes=")
816 .map(|r| r.trim().to_string())
817 });
818 let authorized = text
819 .lines()
820 .any(|l| l.eq_ignore_ascii_case("x-token: secret"));
821
822 let total = data.len();
823 let status_only = |stream: &mut std::net::TcpStream, line: &str| {
824 let _ = stream.write_all(
825 format!("HTTP/1.1 {line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
826 .as_bytes(),
827 );
828 };
829 if matches!(mode, ServerMode::RequireHeader) && !authorized {
830 status_only(&mut stream, "401 Unauthorized");
831 return;
832 }
833 if is_head {
834 let h = if matches!(
835 mode,
836 ServerMode::HeadWithoutLength | ServerMode::HeadWithoutLengthIgnoreRange
837 ) {
838 "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n".to_string()
839 } else {
840 format!(
841 "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nAccept-Ranges: bytes\r\nConnection: close\r\n\r\n"
842 )
843 };
844 let _ = stream.write_all(h.as_bytes());
845 return;
846 }
847 let n = st.gets.fetch_add(1, Ordering::SeqCst) + 1;
848 let now = st.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
849 st.max_in_flight.fetch_max(now, Ordering::SeqCst);
850 std::thread::sleep(delay);
851 st.in_flight.fetch_sub(1, Ordering::SeqCst);
855 let ignores_range = matches!(
856 mode,
857 ServerMode::IgnoreRange
858 | ServerMode::IgnoreRangeUnsized
859 | ServerMode::HeadWithoutLengthIgnoreRange
860 );
861 match (mode, range) {
862 (ServerMode::Error500, _) => status_only(&mut stream, "500 Internal Server Error"),
863 (ServerMode::NotFound, _) => status_only(&mut stream, "404 Not Found"),
864 (ServerMode::Unavailable(k), _) if n <= k => {
865 status_only(&mut stream, "503 Service Unavailable")
866 }
867 (ServerMode::FailAt(at), Some(r)) if r.starts_with(&format!("{at}-")) => {
868 status_only(&mut stream, "404 Not Found")
869 }
870 (ServerMode::IgnoreRangeUnsized, _) => {
871 let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
872 let _ = stream.write_all(data);
873 }
874 (_, Some(r)) if !ignores_range => {
875 let (a, b) = r.split_once('-').unwrap();
876 let a: usize = a.parse().unwrap();
877 let b: usize = b.parse().unwrap();
878 let slice = &data[a.min(total)..=b.min(total - 1)];
879 let claimed = if matches!(mode, ServerMode::WrongOffset) {
880 a + 1
881 } else {
882 a
883 };
884 let h = format!(
885 "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes {claimed}-{b}/{total}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
886 slice.len()
887 );
888 let _ = stream.write_all(h.as_bytes());
889 let sent = if matches!(mode, ServerMode::TruncateBody) {
890 &slice[..slice.len() / 2] } else {
892 slice
893 };
894 let _ = stream.write_all(sent);
895 }
896 _ => {
898 let h = format!(
899 "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nConnection: close\r\n\r\n"
900 );
901 let _ = stream.write_all(h.as_bytes());
902 let _ = stream.write_all(data);
903 }
904 }
905 }
906
907 fn quick() -> FetchPolicy {
909 FetchPolicy {
910 backoff_ms: 1,
911 ..FetchPolicy::DEFAULT
912 }
913 }
914
915 #[test]
916 fn reads_exact_ranges_from_a_range_host() {
917 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
918 let url = serve(data.clone(), ServerMode::HonorRange);
919 let r = HttpRangeReader::open(&url).unwrap();
920 assert_eq!(r.len(), 1000);
921 assert_eq!(r.read_at(0, 4).unwrap(), &data[0..4]);
922 assert_eq!(r.read_at(500, 20).unwrap(), &data[500..520]);
923 assert_eq!(r.read_at(996, 4).unwrap(), &data[996..1000]);
924 assert!(r.read_at(0, 0).unwrap().is_empty());
925 assert!(!r.serves_whole_body());
926 }
927
928 #[test]
932 #[ignore = "hits the network (httpbin); run with --ignored"]
933 fn reads_ranges_over_https() {
934 let url = "https://httpbin.org/range/2048";
935 let r = HttpRangeReader::open(url).unwrap();
936 assert!(r.len() >= 2048);
937 let chunk = r.read_at(100, 16).unwrap();
938 assert_eq!(chunk.len(), 16);
939 }
940
941 #[test]
946 fn keeps_the_whole_body_of_a_host_that_ignores_range() {
947 let data: Vec<u8> = (0..=255u8).cycle().take(5000).collect();
948 let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRange, Duration::ZERO);
949 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
950 assert_eq!(r.len(), 5000);
951 assert_eq!(r.read_at(50, 10).unwrap(), &data[50..60]);
952 assert!(r.serves_whole_body());
953 assert_eq!(r.read_at(4990, 10).unwrap(), &data[4990..5000]);
954 let got = r.read_many(&[(0, 4), (3000, 100), (7, 0)]).unwrap();
955 assert_eq!(got[0], &data[0..4]);
956 assert_eq!(got[1], &data[3000..3100]);
957 assert!(got[2].is_empty());
958 assert_eq!(
959 stats.gets.load(Ordering::SeqCst),
960 1,
961 "one download, then memory"
962 );
963 assert!(r.read_at(4999, 2).is_err());
965 }
966
967 #[test]
970 fn keeps_an_unsized_whole_body_too() {
971 let data: Vec<u8> = (0..=255u8).cycle().take(3000).collect();
972 let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRangeUnsized, Duration::ZERO);
973 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
974 assert_eq!(
975 r.read_many(&[(10, 5), (2000, 5)]).unwrap()[1],
976 &data[2000..2005]
977 );
978 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
979 }
980
981 #[test]
984 fn refuses_a_whole_body_over_the_cap() {
985 let small_cap = FetchPolicy {
986 whole_body_cap: 1000,
987 ..quick()
988 };
989 let data: Vec<u8> = (0..=255u8).cycle().take(4000).collect();
990 for mode in [ServerMode::IgnoreRange, ServerMode::IgnoreRangeUnsized] {
991 let (url, stats) = serve_with(data.clone(), mode, Duration::ZERO);
992 let r = HttpRangeReader::open_with(&url, small_cap).unwrap();
993 let err = r.read_at(50, 10).unwrap_err().to_string();
994 assert!(
995 err.contains("ignored the Range header") && err.contains("not kept in memory"),
996 "{err}"
997 );
998 assert_eq!(stats.gets.load(Ordering::SeqCst), 1, "permanent: no retry");
999 assert!(!r.serves_whole_body());
1000 assert!(r.read_many(&[(0, 1), (2000, 1)]).is_err());
1002 }
1003 assert_eq!(FetchPolicy::DEFAULT.whole_body_cap, 256 << 20);
1005 let msg = whole_body_refusal("u", Some(300 << 20), WHOLE_BODY_CAP).to_string();
1006 assert!(
1007 msg.contains("256 MiB") && msg.contains("status 200"),
1008 "{msg}"
1009 );
1010 }
1011
1012 #[test]
1016 fn measures_the_length_without_a_head_content_length() {
1017 let data: Vec<u8> = (0..=255u8).cycle().take(2048).collect();
1018 let (url, stats) = serve_with(data.clone(), ServerMode::HeadWithoutLength, Duration::ZERO);
1019 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1020 assert_eq!(r.len(), 2048);
1021 assert_eq!(r.read_at(100, 8).unwrap(), &data[100..108]);
1022 assert!(!r.serves_whole_body());
1023 assert_eq!(stats.gets.load(Ordering::SeqCst), 2, "probe + read");
1024
1025 let (url, stats) = serve_with(
1026 data.clone(),
1027 ServerMode::HeadWithoutLengthIgnoreRange,
1028 Duration::ZERO,
1029 );
1030 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1031 assert_eq!(r.len(), 2048);
1032 assert!(r.serves_whole_body());
1033 assert_eq!(r.read_at(2000, 48).unwrap(), &data[2000..2048]);
1034 assert_eq!(
1035 stats.gets.load(Ordering::SeqCst),
1036 1,
1037 "the probe was the file"
1038 );
1039 }
1040
1041 #[test]
1043 fn sends_the_extra_headers_on_every_request() {
1044 let data: Vec<u8> = (0..100u8).collect();
1045 let url = serve(data.clone(), ServerMode::RequireHeader);
1046 assert!(HttpRangeReader::open_with(&url, quick()).is_err());
1047 let headers = vec![("x-token".to_string(), "secret".to_string())];
1048 let r = HttpRangeReader::open_with_headers(&url, headers, quick()).unwrap();
1049 assert_eq!(r.len(), 100);
1050 assert_eq!(r.read_many(&[(0, 2), (90, 10)]).unwrap()[1], &data[90..100]);
1051 }
1052
1053 #[test]
1054 fn rejects_a_truncated_range_response() {
1055 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
1059 let (url, stats) = serve_with(data, ServerMode::TruncateBody, Duration::ZERO);
1060 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1061 let err = r.read_at(100, 40).unwrap_err();
1065 let msg = err.to_string();
1066 assert!(
1067 msg.contains("short range response") || msg.contains("closed before"),
1068 "expected a truncation error, got: {err}"
1069 );
1070 assert_eq!(
1071 stats.gets.load(Ordering::SeqCst),
1072 3,
1073 "a truncated body is transient: retried twice"
1074 );
1075 let data: Vec<u8> = (0..100u8).collect();
1077 let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::ZERO);
1078 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1079 let err = r.read_at(90, 20).unwrap_err();
1080 assert!(err.to_string().contains("out of bounds"), "{err}");
1081 assert!(r.read_many(&[(0, 1), (90, 20)]).is_err());
1082 assert_eq!(stats.gets.load(Ordering::SeqCst), 0);
1083 }
1084
1085 #[test]
1086 fn server_errors_are_clean_errors() {
1087 let data: Vec<u8> = (0..100u8).collect();
1088 let (url, stats) = serve_with(data, ServerMode::Error500, Duration::ZERO);
1089 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1090 let err = r.read_at(0, 10).unwrap_err();
1092 assert!(err.to_string().contains("HTTP 500"), "{err}");
1093 assert!(err.to_string().contains("after 3 attempts"), "{err}");
1094 assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
1095 }
1096
1097 #[test]
1098 fn a_missing_file_is_not_retried() {
1099 let (url, stats) = serve_with(vec![0; 100], ServerMode::NotFound, Duration::ZERO);
1100 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1101 let err = r.read_at(0, 10).unwrap_err();
1102 assert!(err.to_string().contains("HTTP 404"), "{err}");
1103 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
1104 }
1105
1106 #[test]
1107 fn a_transient_failure_is_retried_and_recovers() {
1108 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
1109 let (url, stats) = serve_with(data.clone(), ServerMode::Unavailable(2), Duration::ZERO);
1110 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1111 assert_eq!(r.read_at(10, 30).unwrap(), &data[10..40]);
1112 assert_eq!(stats.gets.load(Ordering::SeqCst), 3, "two 503s, then 206");
1113 let (url, _) = serve_with(data, ServerMode::Unavailable(1), Duration::ZERO);
1115 let none = FetchPolicy {
1116 retries: 0,
1117 ..quick()
1118 };
1119 let r = HttpRangeReader::open_with(&url, none).unwrap();
1120 assert!(r.read_at(10, 30).unwrap_err().to_string().contains("503"));
1121 }
1122
1123 #[test]
1124 fn rejects_a_range_answered_for_the_wrong_offset() {
1125 let (url, _) = serve_with(vec![7; 1000], ServerMode::WrongOffset, Duration::ZERO);
1126 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1127 let err = r.read_at(100, 10).unwrap_err();
1128 assert!(
1129 err.to_string().contains("Content-Range starting at 101"),
1130 "{err}"
1131 );
1132 }
1133
1134 #[test]
1137 fn read_many_overlaps_requests_up_to_the_bound() {
1138 let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
1139 let (url, stats) = serve_with(
1140 data.clone(),
1141 ServerMode::HonorRange,
1142 Duration::from_millis(40),
1143 );
1144 let policy = FetchPolicy {
1145 concurrency: 4,
1146 coalesce_gap: 0,
1147 ..quick()
1148 };
1149 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1150 let mut ranges: Vec<(u64, u64)> = (0..12).map(|i| (i * 65_536 + 7, 100)).collect();
1152 ranges.reverse();
1153 ranges.insert(3, (5, 0));
1154 let t = std::time::Instant::now();
1155 let got = r.read_many(&ranges).unwrap();
1156 let took = t.elapsed();
1157 assert_eq!(got.len(), ranges.len());
1158 for (&(o, l), b) in ranges.iter().zip(&got) {
1159 assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
1160 }
1161 assert_eq!(stats.gets.load(Ordering::SeqCst), 12);
1162 assert_eq!(
1163 stats.max_in_flight.load(Ordering::SeqCst),
1164 4,
1165 "bounded fan-out"
1166 );
1167 assert!(took < Duration::from_millis(12 * 40), "{took:?}");
1169 }
1170
1171 #[test]
1173 fn concurrency_one_is_sequential() {
1174 let data: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
1175 let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::from_millis(5));
1176 let policy = FetchPolicy {
1177 concurrency: 1,
1178 coalesce_gap: 0,
1179 ..quick()
1180 };
1181 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1182 assert_eq!(r.concurrency(), 1);
1183 r.read_many(&[(0, 10), (1000, 10), (3000, 10)]).unwrap();
1184 assert_eq!(stats.max_in_flight.load(Ordering::SeqCst), 1);
1185 assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
1186 }
1187
1188 #[test]
1190 fn read_many_coalesces_nearby_ranges() {
1191 let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
1192 let (url, stats) = serve_with(data.clone(), ServerMode::HonorRange, Duration::ZERO);
1193 let policy = FetchPolicy {
1194 coalesce_gap: 1024,
1195 ..quick()
1196 };
1197 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1198 let ranges = [(5000, 100), (100, 50), (200, 10), (190, 30), (6000, 1)];
1199 let got = r.read_many(&ranges).unwrap();
1200 for (&(o, l), b) in ranges.iter().zip(&got) {
1201 assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
1202 }
1203 assert_eq!(stats.gets.load(Ordering::SeqCst), 2);
1205 }
1206
1207 #[test]
1209 fn read_many_fails_the_batch_on_one_bad_range() {
1210 let data: Vec<u8> = (0..100u8).collect();
1211 let url = serve(data, ServerMode::FailAt(50));
1212 let r = HttpRangeReader::open_with(
1213 &url,
1214 FetchPolicy {
1215 coalesce_gap: 0,
1216 ..quick()
1217 },
1218 )
1219 .unwrap();
1220 let err = r.read_many(&[(0, 10), (50, 10), (20, 10)]).unwrap_err();
1221 assert!(
1222 err.to_string().contains("HTTP 404 for bytes 50-59"),
1223 "{err}"
1224 );
1225 assert!(r.read_many(&[(0, 10), (20, 10)]).is_ok());
1226 }
1227
1228 #[test]
1229 fn coalesce_plans_spans_and_placements() {
1230 let (spans, place) = coalesce(&[(10, 5), (0, 0), (18, 4), (100, 1)], 3, 1 << 20).unwrap();
1231 assert_eq!(
1232 spans,
1233 vec![
1234 Span {
1235 offset: 10,
1236 len: 12
1237 },
1238 Span {
1239 offset: 100,
1240 len: 1
1241 }
1242 ]
1243 );
1244 assert_eq!(place, vec![Some((0, 0)), None, Some((0, 8)), Some((1, 0))]);
1245 let (spans, _) = coalesce(&[(0, 10), (10, 10), (20, 10)], 0, 20).unwrap();
1247 assert_eq!(spans.len(), 2);
1248 assert!(coalesce(&[(u64::MAX, 2)], 0, 10).is_err());
1249 }
1250
1251 #[test]
1252 fn parses_content_range() {
1253 assert_eq!(content_range_start("bytes 100-199/1000"), Some(100));
1254 assert_eq!(content_range_start(" bytes 0-0/*"), Some(0));
1255 assert_eq!(content_range_start("items 1-2/3"), None);
1256 assert_eq!(content_range_total("bytes 0-0/1000"), Some(1000));
1257 assert_eq!(content_range_total("bytes 0-0/*"), None);
1258 }
1259
1260 #[test]
1261 fn retryable_statuses() {
1262 for c in [408, 429, 500, 502, 503, 504] {
1263 assert!(retryable_status(c), "{c}");
1264 }
1265 for c in [400, 403, 404, 416, 501, 505] {
1266 assert!(!retryable_status(c), "{c}");
1267 }
1268 }
1269}