Skip to main content

gwseq_io/source/
url.rs

1//! HTTP/HTTPS byte-range reads.
2//!
3//! `ureq` with `rustls`: pure Rust, no system TLS, nothing for the build to
4//! find or configure on the installing machine.
5//!
6//! # A server that ignores Range
7//!
8//! The single most important thing here, and the reason this is not just "GET
9//! with a header". A server with no range support answers a ranged request
10//! `200` with the **whole file**, and every block would then silently be the
11//! file's first `block_size` bytes. That is not a corner case: Python's own
12//! `http.server` does exactly this. So the response is checked — a `206`, or a
13//! `200` whose body genuinely is the range asked for — and anything else is
14//! refused by name. Because `open()` reads four magic bytes, the refusal lands
15//! there rather than at the first `read_values`.
16//!
17//! # Retries
18//!
19//! `ureq` gives none of this; every rule below is this module's own.
20//!
21//! - Retry only transient failures: timeouts, refused or reset connections,
22//!   truncated bodies, unresolved hosts, and HTTP 408 / 425 / 429 / 5xx. A bad
23//!   URL, a 404 or a rejected certificate fails on the first attempt.
24//! - Backoff doubles per attempt (`delay * 2^n`), capped at `max_delay`, and is
25//!   jittered over `[d/2, d]` so parallel readers do not resynchronise on one
26//!   server.
27//! - A `Retry-After` header carrying a seconds count overrides the computed
28//!   delay, still capped.
29//! - A retry re-fetches its whole range rather than resuming.
30
31use std::ops::Range;
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::sync::OnceLock;
34use std::time::Duration;
35
36use bytes::Bytes;
37
38use crate::error::{Error, Result};
39use crate::source::ByteSource;
40
41#[derive(Debug, Clone)]
42pub struct RetryPolicy {
43    /// Retries *after* the first attempt, so a request is issued at most
44    /// `max_retries + 1` times. 0 disables retrying.
45    pub max_retries: u32,
46    pub delay: Duration,
47    pub max_delay: Duration,
48    pub connect_timeout: Duration,
49    /// Backstop against a transfer that trickles. The right rule is a
50    /// *throughput* bound — abort after 30 s below 1 KB/s — but `ureq` has no
51    /// such setting; a whole-transfer deadline is the closest thing, and it is
52    /// deliberately generous so a large but healthy read is never cut off for
53    /// its size.
54    pub timeout: Duration,
55}
56
57impl Default for RetryPolicy {
58    fn default() -> Self {
59        Self {
60            max_retries: 3,
61            delay: Duration::from_millis(500),
62            max_delay: Duration::from_secs(30),
63            connect_timeout: Duration::from_secs(10),
64            timeout: Duration::from_secs(600),
65        }
66    }
67}
68
69#[derive(Debug)]
70pub struct UrlSource {
71    url: String,
72    agent: ureq::Agent,
73    policy: RetryPolicy,
74    /// From a `HEAD`. Cached: it costs a round trip, and these files do not
75    /// change under a reader.
76    len: OnceLock<u64>,
77    closed: AtomicBool,
78}
79
80/// What one attempt came back with.
81#[derive(Debug)]
82struct Response {
83    status: u16,
84    body: Vec<u8>,
85    content_length: Option<u64>,
86    /// The raw `Content-Range`, for the size of a resource whose server will
87    /// not answer `HEAD`. See [`UrlSource::len_from_range`].
88    content_range: Option<String>,
89    retry_after: Option<Duration>,
90}
91
92impl UrlSource {
93    pub fn open(url: &str) -> Result<Self> {
94        Self::open_with(url, RetryPolicy::default())
95    }
96
97    pub fn open_with(url: &str, policy: RetryPolicy) -> Result<Self> {
98        let agent: ureq::Agent = ureq::Agent::config_builder()
99            .timeout_connect(Some(policy.connect_timeout))
100            .timeout_global(Some(policy.timeout))
101            // Statuses are inspected here rather than raised by the client: the
102            // retry rules need the code, and `Retry-After` only arrives with the
103            // response that carries it.
104            .http_status_as_error(false)
105            .user_agent(concat!("gwseq-io/", env!("CARGO_PKG_VERSION")))
106            .build()
107            .into();
108        Ok(Self {
109            url: url.to_string(),
110            agent,
111            policy,
112            len: OnceLock::new(),
113            closed: AtomicBool::new(false),
114        })
115    }
116
117    fn check_open(&self) -> Result<()> {
118        if self.closed.load(Ordering::Relaxed) {
119            return Err(Error::Closed {
120                path: self.url.clone(),
121            });
122        }
123        Ok(())
124    }
125
126    fn http_error(&self, status: Option<u16>, message: impl Into<String>) -> Error {
127        Error::Http {
128            url: self.url.clone(),
129            status,
130            message: message.into(),
131        }
132    }
133
134    /// One attempt, with an optional `Range` header. Transport failures come
135    /// back as `Err`; an HTTP status of any value comes back as `Ok`.
136    ///
137    /// `cap` bounds the body. Nothing here reads a body of unknown size: a
138    /// range asks for a known number of bytes and `http_get_text` names its
139    /// own limit, so a server — hostile, or merely broken — cannot make this
140    /// grow until the process dies. Three responses have their body skipped
141    /// entirely rather than capped: a `HEAD`, which has none; an error status,
142    /// whose body nothing reads; and a ranged request the server answered with
143    /// something other than the range, which `check_range_response` is about
144    /// to refuse. That last one is the point — the module's headline case, a
145    /// server that ignores `Range` and sends the whole resource, used to cost
146    /// a transfer of the file *per block* before the refusal.
147    fn attempt(
148        &self,
149        range: Option<Range<u64>>,
150        head: bool,
151        cap: u64,
152    ) -> std::result::Result<Response, ureq::Error> {
153        let mut request = if head {
154            self.agent.head(&self.url)
155        } else {
156            self.agent.get(&self.url)
157        };
158        if let Some(range) = &range {
159            request = request.header("Range", format!("bytes={}-{}", range.start, range.end - 1));
160        }
161        let mut response = request.call()?;
162
163        let status = response.status().as_u16();
164        let header = |name: &str| {
165            response
166                .headers()
167                .get(name)
168                .and_then(|v| v.to_str().ok())
169                .map(str::to_string)
170        };
171        let content_length = header("content-length").and_then(|v| v.parse().ok());
172        let content_range = header("content-range");
173        // Only the seconds form; the HTTP-date form is legal and rare, and
174        // guessing at a clock skew is worse than falling back to the backoff.
175        let retry_after = header("retry-after")
176            .and_then(|v| v.trim().parse::<u64>().ok())
177            .map(Duration::from_secs);
178
179        let unusable_range = range
180            .as_ref()
181            .is_some_and(|r| !range_response_is_usable(status, content_length, r));
182        let body = if head || status >= 400 || unusable_range {
183            Vec::new()
184        } else {
185            // `Read::take`, not ureq's own `limit`: that one errors past the
186            // cap, and this module deliberately tolerates a server answering
187            // with more than was asked for — the extra is simply not part of
188            // the range. Nothing drains what is left, so the transfer stops
189            // where the read does.
190            use std::io::Read as _;
191            let mut buf = Vec::new();
192            response
193                .body_mut()
194                .with_config()
195                .reader()
196                .take(cap)
197                .read_to_end(&mut buf)?;
198            buf
199        };
200        Ok(Response {
201            status,
202            body,
203            content_length,
204            content_range,
205            retry_after,
206        })
207    }
208
209    /// The retry loop around [`Self::attempt`].
210    fn perform(
211        &self,
212        what: &str,
213        range: Option<Range<u64>>,
214        head: bool,
215        cap: u64,
216    ) -> Result<Response> {
217        self.check_open()?;
218        let mut attempt = 0u32;
219        loop {
220            let outcome = self.attempt(range.clone(), head, cap);
221            let (transient, retry_after, failure) = match &outcome {
222                Ok(response) if response.status < 400 => {
223                    return Ok(outcome.expect("checked Ok above"))
224                }
225                Ok(response) => (
226                    is_retryable_status(response.status),
227                    response.retry_after,
228                    self.http_error(
229                        Some(response.status),
230                        format!("{what} failed with HTTP {}", response.status),
231                    ),
232                ),
233                Err(error) => (
234                    is_retryable_transport(error),
235                    None,
236                    self.http_error(None, format!("{what} failed: {error}")),
237                ),
238            };
239
240            if attempt >= self.policy.max_retries || !transient {
241                return Err(failure);
242            }
243            std::thread::sleep(self.backoff(attempt, retry_after));
244            attempt += 1;
245        }
246    }
247
248    /// How long to wait before the retry following attempt `n` (0-based).
249    ///
250    /// A `Retry-After` from the server wins; otherwise the delay doubles per
251    /// attempt. Both are capped, and the exponential form is jittered over
252    /// `[d/2, d]` so parallel readers hitting one server do not resynchronise.
253    fn backoff(&self, attempt: u32, retry_after: Option<Duration>) -> Duration {
254        if let Some(after) = retry_after {
255            return after.min(self.policy.max_delay);
256        }
257        let delay = self
258            .policy
259            .delay
260            .saturating_mul(1u32 << attempt.min(16))
261            .min(self.policy.max_delay);
262        let half = delay.as_nanos() as u64 / 2;
263        Duration::from_nanos(half + jitter(half + 1))
264    }
265
266    /// The resource's size from a one-byte ranged GET, for a server that will
267    /// not answer `HEAD`.
268    ///
269    /// `Content-Range: bytes 0-0/12345` carries the total after the slash. A
270    /// `*` there means the server does not know it, which is no more use than
271    /// the missing `Content-Length` that led here.
272    fn len_from_range(&self) -> Result<u64> {
273        let response = self.perform("determining the size of", Some(0..1), false, 1)?;
274        let total = response
275            .content_range
276            .as_deref()
277            .and_then(parse_content_range_total)
278            // A server with no ranges at all answers this `200` with the whole
279            // resource, and its `Content-Length` is then the total — the one
280            // thing worth taking from a response the reads themselves will
281            // refuse. On a `206` the same header is the length of the range,
282            // which is 1 and says nothing, so this arm is only for the `200`.
283            .or(if response.status == 200 {
284                response.content_length
285            } else {
286                None
287            })
288            .ok_or_else(|| {
289                self.http_error(
290                    Some(response.status),
291                    "no content length, and no total in the content range either",
292                )
293            })?;
294        Ok(total)
295    }
296
297    /// Refuse a server that answered a ranged request with something other than
298    /// the range asked for.
299    fn check_range_response(&self, response: &Response, range: &Range<u64>) -> Result<()> {
300        if range_response_is_usable(response.status, response.content_length, range) {
301            return Ok(());
302        }
303        Err(self.http_error(
304            Some(response.status),
305            format!(
306                "the server ignored the Range header and answered bytes {}-{} with the whole \
307                 resource, so the bytes it sent are not the ones asked for",
308                range.start,
309                range.end - 1
310            ),
311        ))
312    }
313}
314
315/// The total size out of a `Content-Range: bytes 0-0/12345`.
316///
317/// `None` for the `*` form, which is the server saying it does not know, and
318/// for anything that does not parse — a header this reader cannot understand is
319/// not a number worth guessing at.
320fn parse_content_range_total(header: &str) -> Option<u64> {
321    header.rsplit_once('/')?.1.trim().parse().ok()
322}
323
324/// Whether a response to a ranged request carries the bytes that were asked for.
325///
326/// A `206` does by definition. A `200` only when the whole resource *is* the
327/// range asked for — an offset of 0, and a request reaching the end of the
328/// file; a `200` with no content length says nothing that can be trusted.
329///
330/// One rule, read twice: [`UrlSource::attempt`] uses it to decide whether the
331/// body is worth transferring at all, and [`UrlSource::check_range_response`]
332/// to turn the same answer into the error the caller sees.
333fn range_response_is_usable(status: u16, content_length: Option<u64>, range: &Range<u64>) -> bool {
334    if status == 206 {
335        return true;
336    }
337    status == 200 && range.start == 0 && content_length.is_some_and(|length| range.end >= length)
338}
339
340/// Retryable statuses: 408, 425, 429 and anything 5xx.
341fn is_retryable_status(status: u16) -> bool {
342    matches!(status, 408 | 425 | 429) || status >= 500
343}
344
345/// The transport failures worth retrying.
346///
347/// Refused and reset connections, unresolved hosts, timeouts, truncated bodies
348/// — everything that says "try again", and nothing that says "this will not
349/// work": a bad URI, a rejected certificate, a redirect loop.
350fn is_retryable_transport(error: &ureq::Error) -> bool {
351    use std::io::ErrorKind::*;
352    match error {
353        ureq::Error::Timeout(_) | ureq::Error::ConnectionFailed | ureq::Error::HostNotFound => true,
354        ureq::Error::Io(io) => matches!(
355            io.kind(),
356            TimedOut
357                | ConnectionRefused
358                | ConnectionReset
359                | ConnectionAborted
360                | NotConnected
361                | BrokenPipe
362                | UnexpectedEof
363                | Interrupted
364                | WouldBlock
365        ),
366        _ => false,
367    }
368}
369
370/// A jitter in `[0, bound)`, from the clock. Not cryptographic and not meant to
371/// be: the point is only that two readers do not wake together.
372fn jitter(bound: u64) -> u64 {
373    if bound == 0 {
374        return 0;
375    }
376    let nanos = std::time::SystemTime::now()
377        .duration_since(std::time::UNIX_EPOCH)
378        .map(|d| d.subsec_nanos() as u64)
379        .unwrap_or(0);
380    // Mixed with the thread id, so two threads sleeping in the same nanosecond
381    // still separate.
382    let id = {
383        use std::hash::{Hash, Hasher};
384        let mut hasher = std::collections::hash_map::DefaultHasher::new();
385        std::thread::current().id().hash(&mut hasher);
386        hasher.finish()
387    };
388    (nanos ^ id) % bound
389}
390
391impl ByteSource for UrlSource {
392    fn path(&self) -> &str {
393        &self.url
394    }
395
396    fn len(&self) -> Result<u64> {
397        if let Some(length) = self.len.get() {
398            return Ok(*length);
399        }
400        let length = match self.perform("determining the size of", None, true, 0) {
401            Ok(response) => response.content_length,
402            // A server that refuses HEAD outright — 405, or 501 from one that
403            // implements only GET — is common enough that failing here would
404            // make the whole file unreadable over a plain object store. The
405            // fallback is the usual one: ask for a single byte and read the
406            // size out of the `Content-Range` the 206 carries.
407            Err(error) => {
408                let refused_head = matches!(
409                    &error,
410                    Error::Http {
411                        status: Some(405 | 501),
412                        ..
413                    }
414                );
415                if !refused_head {
416                    return Err(error);
417                }
418                None
419            }
420        };
421        let length = match length {
422            Some(length) => length,
423            None => self.len_from_range()?,
424        };
425        Ok(*self.len.get_or_init(|| length))
426    }
427
428    fn read_at(&self, offset: u64, len: usize) -> Result<Bytes> {
429        self.check_open()?;
430        if len == 0 {
431            return Ok(Bytes::new());
432        }
433        // HTTP has no empty range — a server answers one with 416, a hard error
434        // not worth retrying — so a read starting at or past the end is
435        // resolved here rather than sent, as a local file resolves it.
436        let file_size = self.len()?;
437        if offset >= file_size {
438            return Ok(Bytes::new());
439        }
440
441        let range = offset..offset + len as u64;
442        let response = self.perform("reading", Some(range.clone()), false, len as u64)?;
443        self.check_range_response(&response, &range)?;
444
445        let mut body = response.body;
446        // A server may answer with more than was asked for; the extra is not
447        // part of the range and is dropped.
448        body.truncate(len);
449        Ok(Bytes::from(body))
450    }
451
452    /// Left as a no-op. Coalescing the R-tree's leaves into multi-range
453    /// requests is worth measuring before it is worth writing.
454    fn prefetch(&self, _ranges: &[Range<u64>]) {}
455
456    fn close(&self) {
457        self.closed.store(true, Ordering::Relaxed);
458    }
459}
460
461/// The most a [`http_get_text`] body may be, in bytes.
462///
463/// Its one caller is the UCSC chromosome-sizes API, whose largest answers are
464/// an assembly of a few hundred thousand scaffolds — a few megabytes of text.
465/// The cap is here because that call has no range to bound it, so without one
466/// the size of the response is whatever the far end decides to send.
467const MAX_TEXT_BODY: u64 = 64 << 20;
468
469/// A plain GET returning the body as text.
470///
471/// Not part of [`ByteSource`] — nothing about reading a bigWig needs it. It is
472/// here because this is the module that owns the HTTP client, and the UCSC
473/// genome API is the one caller.
474pub fn http_get_text(url: &str) -> Result<String> {
475    let source = UrlSource::open(url)?;
476    let response = source.perform("fetching", None, false, MAX_TEXT_BODY)?;
477    String::from_utf8(response.body).map_err(|_| Error::Http {
478        url: url.to_string(),
479        status: Some(response.status),
480        message: "response was not valid UTF-8".to_string(),
481    })
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn the_statuses_worth_retrying_are_the_documented_ones() {
490        for status in [408, 425, 429, 500, 502, 503, 504, 599] {
491            assert!(is_retryable_status(status), "{status}");
492        }
493        for status in [400, 401, 403, 404, 410, 416, 451] {
494            assert!(!is_retryable_status(status), "{status}");
495        }
496    }
497
498    #[test]
499    fn backoff_doubles_and_stays_inside_its_jitter_window() {
500        let source = UrlSource::open("http://127.0.0.1:1/x").unwrap();
501        for attempt in 0..4u32 {
502            let nominal = (source.policy.delay.as_secs_f64() * 2f64.powi(attempt as i32))
503                .min(source.policy.max_delay.as_secs_f64());
504            let got = source.backoff(attempt, None).as_secs_f64();
505            assert!(
506                got >= nominal / 2.0 - 1e-6 && got <= nominal + 1e-6,
507                "attempt {attempt}: {got} outside [{}, {nominal}]",
508                nominal / 2.0
509            );
510        }
511    }
512
513    #[test]
514    fn backoff_is_capped_however_many_attempts() {
515        let source = UrlSource::open("http://127.0.0.1:1/x").unwrap();
516        for attempt in [10u32, 20, 40] {
517            assert!(source.backoff(attempt, None) <= source.policy.max_delay);
518        }
519    }
520
521    #[test]
522    fn retry_after_wins_over_the_backoff_but_is_still_capped() {
523        let source = UrlSource::open("http://127.0.0.1:1/x").unwrap();
524        assert_eq!(
525            source.backoff(0, Some(Duration::from_secs(7))),
526            Duration::from_secs(7)
527        );
528        assert_eq!(
529            source.backoff(0, Some(Duration::from_secs(9999))),
530            source.policy.max_delay
531        );
532    }
533
534    fn response(status: u16, content_length: Option<u64>) -> Response {
535        Response {
536            status,
537            body: Vec::new(),
538            content_length,
539            content_range: None,
540            retry_after: None,
541        }
542    }
543
544    #[test]
545    fn a_range_answered_with_the_whole_file_is_refused() {
546        let source = UrlSource::open("http://example.invalid/x").unwrap();
547        // The block a reader actually asks for: not from the start, and not
548        // the whole file.
549        let err = source
550            .check_range_response(&response(200, Some(1000)), &(4096..8192))
551            .unwrap_err()
552            .to_string();
553        assert!(err.contains("ignored the Range header"), "{err}");
554        assert!(err.contains("bytes 4096-8191"), "{err}");
555    }
556
557    #[test]
558    fn a_206_is_always_fine_and_a_200_only_when_it_is_the_whole_file() {
559        let source = UrlSource::open("http://example.invalid/x").unwrap();
560        assert!(source
561            .check_range_response(&response(206, Some(100)), &(4096..8192))
562            .is_ok());
563        // A file of 1000 bytes asked for from 0 with a request reaching past
564        // the end: the whole resource *is* the range.
565        assert!(source
566            .check_range_response(&response(200, Some(1000)), &(0..4096))
567            .is_ok());
568        // The same file asked for from 0 but only in part is not.
569        assert!(source
570            .check_range_response(&response(200, Some(1000)), &(0..500))
571            .is_err());
572        // And a 200 with no content length says nothing that can be trusted.
573        assert!(source
574            .check_range_response(&response(200, None), &(0..4096))
575            .is_err());
576    }
577
578    #[test]
579    fn a_closed_source_refuses_and_names_the_url() {
580        let source = UrlSource::open("http://127.0.0.1:1/x.bigwig").unwrap();
581        source.close();
582        source.close(); // idempotent
583        let err = source.read_at(0, 4).unwrap_err().to_string();
584        assert!(err.contains("is closed"), "{err}");
585        assert!(err.contains("x.bigwig"), "{err}");
586    }
587
588    #[test]
589    fn a_zero_length_read_never_leaves_the_process() {
590        // Nothing is listening on port 1; a request here would fail.
591        let source = UrlSource::open("http://127.0.0.1:1/x.bigwig").unwrap();
592        assert!(source.read_at(0, 0).unwrap().is_empty());
593    }
594
595    // ---------------------------------------------------------------------------
596    // A server to read from
597    // ---------------------------------------------------------------------------
598    //
599    // Everything above this point was reviewed by reading, because there was no
600    // way to run it: the suite has no fixture and no network, and the one place a
601    // wrong answer here is expensive — a server that ignores `Range` — cannot be
602    // reached without a server that ignores `Range`.
603    //
604    // So here is one. A `TcpListener` on a loopback port the OS picks, a thread per
605    // connection, and a handful of canned behaviours. It needs no fixture, no
606    // network beyond loopback, and no port anyone else could be holding.
607
608    use std::io::{BufRead, BufReader, Write};
609    use std::net::{TcpListener, TcpStream};
610    use std::sync::atomic::AtomicU64;
611    use std::sync::Arc;
612
613    /// What the test server does with a request.
614    #[derive(Clone, Copy, Debug)]
615    enum Behaviour {
616        /// A correct server: `206` with the bytes asked for, `HEAD` answered.
617        Ranged,
618        /// Ignores `Range` and answers `200` with the whole resource. Python's own
619        /// `http.server` does exactly this, which is why the module has a check for
620        /// it at all.
621        IgnoresRange,
622        /// Refuses `HEAD` with 405 and is otherwise correct.
623        NoHead,
624        /// Answers every GET `200` with a body far larger than it declares, and
625        /// keeps writing. What a cap is for.
626        Endless,
627        /// The worst of both: no `HEAD`, and no attention paid to `Range`. Its
628        /// length is still knowable, and its reads still have to be refused.
629        NoHeadNoRange,
630    }
631
632    struct TestServer {
633        port: u16,
634        /// Body bytes the server managed to write before the client stopped
635        /// reading. What says whether a transfer really was avoided.
636        written: Arc<AtomicU64>,
637        stop: Arc<AtomicBool>,
638    }
639
640    impl TestServer {
641        fn start(behaviour: Behaviour, body: Vec<u8>) -> Self {
642            let listener = TcpListener::bind("127.0.0.1:0").expect("a loopback port");
643            let port = listener.local_addr().expect("an address").port();
644            let written = Arc::new(AtomicU64::new(0));
645            let stop = Arc::new(AtomicBool::new(false));
646            let (w, s) = (written.clone(), stop.clone());
647            std::thread::spawn(move || {
648                for stream in listener.incoming() {
649                    if s.load(Ordering::Relaxed) {
650                        break;
651                    }
652                    let Ok(stream) = stream else { break };
653                    let (w, body) = (w.clone(), body.clone());
654                    std::thread::spawn(move || {
655                        let _ = serve(stream, behaviour, &body, &w);
656                    });
657                }
658            });
659            Self {
660                port,
661                written,
662                stop,
663            }
664        }
665
666        fn url(&self) -> String {
667            format!("http://127.0.0.1:{}/x.bigwig", self.port)
668        }
669
670        fn body_bytes_written(&self) -> u64 {
671            self.written.load(Ordering::Relaxed)
672        }
673
674        /// A source with no retries and short timeouts: a test that is going to
675        /// fail should fail now, not in half a minute.
676        fn source(&self) -> UrlSource {
677            UrlSource::open_with(
678                &self.url(),
679                RetryPolicy {
680                    max_retries: 0,
681                    delay: Duration::from_millis(1),
682                    max_delay: Duration::from_millis(1),
683                    connect_timeout: Duration::from_secs(2),
684                    timeout: Duration::from_secs(10),
685                },
686            )
687            .expect("a source")
688        }
689    }
690
691    impl Drop for TestServer {
692        fn drop(&mut self) {
693            self.stop.store(true, Ordering::Relaxed);
694            // Wake the accept loop so the thread notices and leaves.
695            let _ = TcpStream::connect(("127.0.0.1", self.port));
696        }
697    }
698
699    fn serve(
700        mut stream: TcpStream,
701        behaviour: Behaviour,
702        body: &[u8],
703        written: &AtomicU64,
704    ) -> std::io::Result<()> {
705        let mut reader = BufReader::new(stream.try_clone()?);
706        let mut request = String::new();
707        reader.read_line(&mut request)?;
708        let mut range: Option<(u64, u64)> = None;
709        loop {
710            let mut line = String::new();
711            if reader.read_line(&mut line)? == 0 || line.trim().is_empty() {
712                break;
713            }
714            if let Some(spec) = line.to_ascii_lowercase().strip_prefix("range: bytes=") {
715                if let Some((a, b)) = spec.trim().split_once('-') {
716                    if let (Ok(a), Ok(b)) = (a.parse::<u64>(), b.parse::<u64>()) {
717                        range = Some((a, b));
718                    }
719                }
720            }
721        }
722        let head = request.starts_with("HEAD ");
723        let total = body.len() as u64;
724
725        if head && matches!(behaviour, Behaviour::NoHead | Behaviour::NoHeadNoRange) {
726            return stream
727                .write_all(b"HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n");
728        }
729        if head {
730            return stream.write_all(
731                format!(
732                    "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nAccept-Ranges: bytes\r\n\r\n"
733                )
734                .as_bytes(),
735            );
736        }
737
738        if matches!(behaviour, Behaviour::Endless) {
739            // The size it declares, and then rather more than that.
740            stream.write_all(
741                format!("HTTP/1.1 200 OK\r\nContent-Length: {total}\r\n\r\n").as_bytes(),
742            )?;
743            let chunk = vec![b'z'; 64 << 10];
744            loop {
745                match stream.write(&chunk) {
746                    Ok(0) | Err(_) => return Ok(()),
747                    Ok(n) => {
748                        written.fetch_add(n as u64, Ordering::Relaxed);
749                        // Bounded so a test that reads everything fails by
750                        // assertion rather than by running forever.
751                        if written.load(Ordering::Relaxed) > 256 << 20 {
752                            return Ok(());
753                        }
754                    }
755                }
756            }
757        }
758
759        let ignores = matches!(
760            behaviour,
761            Behaviour::IgnoresRange | Behaviour::NoHeadNoRange
762        );
763        match range {
764            Some((start, end)) if !ignores => {
765                let end = end.min(total.saturating_sub(1));
766                let slice = &body[start as usize..=(end as usize)];
767                stream.write_all(
768                    format!(
769                        "HTTP/1.1 206 Partial Content\r\nContent-Length: {}\r\n\
770                     Content-Range: bytes {start}-{end}/{total}\r\n\r\n",
771                        slice.len()
772                    )
773                    .as_bytes(),
774                )?;
775                stream.write_all(slice)?;
776                written.fetch_add(slice.len() as u64, Ordering::Relaxed);
777                Ok(())
778            }
779            // No range asked for, or a server that pays no attention to one.
780            _ => {
781                stream.write_all(
782                    format!("HTTP/1.1 200 OK\r\nContent-Length: {total}\r\n\r\n").as_bytes(),
783                )?;
784                // Written in pieces so a client that stops reading stops the
785                // transfer, which is the whole thing under test.
786                for piece in body.chunks(64 << 10) {
787                    match stream.write(piece) {
788                        Ok(0) | Err(_) => return Ok(()),
789                        Ok(n) => {
790                            written.fetch_add(n as u64, Ordering::Relaxed);
791                        }
792                    }
793                }
794                Ok(())
795            }
796        }
797    }
798
799    fn payload(n: usize) -> Vec<u8> {
800        (0..n).map(|i| (i % 251) as u8).collect()
801    }
802
803    #[test]
804    fn a_correct_server_serves_the_bytes_that_were_asked_for() {
805        let body = payload(100_000);
806        let server = TestServer::start(Behaviour::Ranged, body.clone());
807        let source = server.source();
808        assert_eq!(source.len().unwrap(), 100_000);
809        for (offset, len) in [(0u64, 16usize), (4096, 4096), (99_990, 10)] {
810            let got = source.read_at(offset, len).unwrap();
811            assert_eq!(&got[..], &body[offset as usize..offset as usize + len]);
812        }
813        // The length is cached, so a second read costs no second HEAD.
814        assert_eq!(source.len().unwrap(), 100_000);
815    }
816
817    #[test]
818    fn a_server_that_ignores_range_is_refused_without_transferring_the_file() {
819        // The module's headline case. The refusal was always there; what was not is
820        // that it cost a download of the whole resource *per block* to reach it.
821        let body = payload(4 << 20);
822        let server = TestServer::start(Behaviour::IgnoresRange, body);
823        let source = server.source();
824
825        let err = source.read_at(1 << 20, 4096).unwrap_err();
826        let text = err.to_string();
827        assert!(text.contains("ignored the Range header"), "{text}");
828
829        // The HEAD that resolved the length sends no body, and the GET was refused
830        // on its status line. A few tens of kilobytes may already be in flight when
831        // the socket closes; four megabytes means the file was downloaded.
832        let written = server.body_bytes_written();
833        assert!(
834            written < 1 << 20,
835            "{written} body bytes crossed the socket for a request that was refused"
836        );
837    }
838
839    #[test]
840    fn a_body_larger_than_the_range_asked_for_is_cut_off() {
841        // A server whose body outruns what it declared, which is the shape of an
842        // unbounded response: the read stops at the cap rather than growing to
843        // whatever the far end feels like sending.
844        let server = TestServer::start(Behaviour::Endless, payload(64));
845        let source = server.source();
846        // 64 bytes declared, so a 64-byte read from 0 is the whole resource and the
847        // 200 is accepted — and then the body keeps coming.
848        let got = source.read_at(0, 64).unwrap();
849        assert_eq!(got.len(), 64, "the read grew past the range it asked for");
850        let written = server.body_bytes_written();
851        assert!(
852            written < 32 << 20,
853            "{written} bytes were taken for a 64-byte read"
854        );
855    }
856
857    #[test]
858    fn a_server_that_refuses_head_still_gives_up_its_length() {
859        let body = payload(54_321);
860        let server = TestServer::start(Behaviour::NoHead, body.clone());
861        let source = server.source();
862        // 405 on HEAD, so the size comes out of a one-byte range's Content-Range.
863        assert_eq!(source.len().unwrap(), 54_321);
864        let got = source.read_at(50_000, 321).unwrap();
865        assert_eq!(&got[..], &body[50_000..50_321]);
866    }
867
868    #[test]
869    fn a_server_with_neither_head_nor_ranges_still_gives_up_its_length() {
870        // The length comes out of the `Content-Length` of the `200` the
871        // one-byte range provoked, since there is no `Content-Range` to read it
872        // from. The reads themselves are still refused — knowing how long a
873        // file is does not make its bytes arrive in the right order.
874        let server = TestServer::start(Behaviour::NoHeadNoRange, payload(4321));
875        let source = server.source();
876        assert_eq!(source.len().unwrap(), 4321);
877        let err = source.read_at(100, 16).unwrap_err().to_string();
878        assert!(err.contains("ignored the Range header"), "{err}");
879    }
880
881    #[test]
882    fn a_read_past_the_end_is_empty_and_a_read_over_it_is_short() {
883        let body = payload(1000);
884        let server = TestServer::start(Behaviour::Ranged, body.clone());
885        let source = server.source();
886        assert!(source.read_at(1000, 16).unwrap().is_empty());
887        assert!(source.read_at(5000, 16).unwrap().is_empty());
888        let got = source.read_at(990, 100).unwrap();
889        assert_eq!(&got[..], &body[990..]);
890    }
891
892    #[test]
893    fn the_content_range_total_is_read_only_when_it_is_a_number() {
894        assert_eq!(parse_content_range_total("bytes 0-0/12345"), Some(12345));
895        assert_eq!(parse_content_range_total("bytes 0-0/*"), None);
896        assert_eq!(parse_content_range_total("nonsense"), None);
897        assert_eq!(parse_content_range_total(""), None);
898    }
899}