Skip to main content

fastapi_http/
range.rs

1//! HTTP Range request parsing and response generation (RFC 7233).
2//!
3//! This module provides support for:
4//!
5//! - Parsing `Range` headers (bytes=start-end, bytes=start-, bytes=-suffix)
6//! - Validating ranges against resource sizes
7//! - Generating `Content-Range` headers
8//! - Building 206 Partial Content responses
9//! - Handling 416 Range Not Satisfiable errors
10//!
11//! # Example
12//!
13//! ```ignore
14//! use fastapi_http::range::{Range, parse_range_header};
15//!
16//! let range_header = "bytes=0-499";
17//! let file_size = 1000;
18//!
19//! match parse_range_header(range_header, file_size) {
20//!     Ok(ranges) => {
21//!         // Handle partial content response
22//!         for range in ranges {
23//!             println!("Serve bytes {}-{}", range.start, range.end);
24//!         }
25//!     }
26//!     Err(e) => {
27//!         // Return 416 Range Not Satisfiable
28//!     }
29//! }
30//! ```
31
32use std::fmt;
33
34/// A validated byte range within a resource.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct ByteRange {
37    /// Start byte offset (inclusive).
38    pub start: u64,
39    /// End byte offset (inclusive).
40    pub end: u64,
41}
42
43impl ByteRange {
44    /// Create a new byte range.
45    ///
46    /// # Panics
47    ///
48    /// Panics if start > end.
49    #[must_use]
50    pub fn new(start: u64, end: u64) -> Self {
51        assert!(start <= end, "start must be <= end");
52        Self { start, end }
53    }
54
55    /// Get the length of this range in bytes.
56    #[must_use]
57    pub fn len(&self) -> u64 {
58        self.end.saturating_sub(self.start).saturating_add(1)
59    }
60
61    /// Check if the range is empty (zero length).
62    #[must_use]
63    pub fn is_empty(&self) -> bool {
64        false // A valid ByteRange always has at least 1 byte
65    }
66
67    /// Format as a Content-Range header value.
68    ///
69    /// Returns a string like "bytes 0-499/1000".
70    #[must_use]
71    pub fn content_range_header(&self, total_size: u64) -> String {
72        format!("bytes {}-{}/{}", self.start, self.end, total_size)
73    }
74}
75
76impl fmt::Display for ByteRange {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}-{}", self.start, self.end)
79    }
80}
81
82/// Errors that can occur when parsing or validating range requests.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum RangeError {
85    /// The Range header syntax is invalid.
86    InvalidSyntax(String),
87    /// The range unit is not "bytes".
88    UnsupportedUnit(String),
89    /// The range is not satisfiable for the given resource size.
90    NotSatisfiable {
91        /// The size of the resource.
92        resource_size: u64,
93    },
94    /// Multiple ranges requested (not yet supported).
95    MultipleRangesNotSupported,
96}
97
98impl fmt::Display for RangeError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            Self::InvalidSyntax(msg) => write!(f, "invalid range syntax: {msg}"),
102            Self::UnsupportedUnit(unit) => write!(f, "unsupported range unit: {unit}"),
103            Self::NotSatisfiable { resource_size } => {
104                write!(
105                    f,
106                    "range not satisfiable for resource of size {resource_size}"
107                )
108            }
109            Self::MultipleRangesNotSupported => write!(f, "multiple ranges not supported"),
110        }
111    }
112}
113
114impl std::error::Error for RangeError {}
115
116/// A parsed range specification before validation against resource size.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum RangeSpec {
119    /// bytes=start-end (both specified).
120    FromTo { start: u64, end: u64 },
121    /// bytes=start- (from start to end of resource).
122    From { start: u64 },
123    /// bytes=-suffix (last N bytes).
124    Suffix { length: u64 },
125}
126
127impl RangeSpec {
128    /// Validate and resolve this range specification against a resource size.
129    ///
130    /// Returns a concrete `ByteRange` if the range is satisfiable.
131    ///
132    /// # Errors
133    ///
134    /// Returns `RangeError::NotSatisfiable` if the range cannot be satisfied.
135    pub fn resolve(self, resource_size: u64) -> Result<ByteRange, RangeError> {
136        if resource_size == 0 {
137            return Err(RangeError::NotSatisfiable { resource_size });
138        }
139
140        match self {
141            Self::FromTo { start, end } => {
142                // RFC 7233: If the last-byte-pos is >= resource size, use resource_size - 1
143                let end = end.min(resource_size - 1);
144
145                if start > end || start >= resource_size {
146                    return Err(RangeError::NotSatisfiable { resource_size });
147                }
148
149                Ok(ByteRange::new(start, end))
150            }
151            Self::From { start } => {
152                if start >= resource_size {
153                    return Err(RangeError::NotSatisfiable { resource_size });
154                }
155                Ok(ByteRange::new(start, resource_size - 1))
156            }
157            Self::Suffix { length } => {
158                if length == 0 {
159                    return Err(RangeError::NotSatisfiable { resource_size });
160                }
161                // Last N bytes
162                let start = resource_size.saturating_sub(length);
163                Ok(ByteRange::new(start, resource_size - 1))
164            }
165        }
166    }
167}
168
169/// Parse a Range header value and resolve it against a resource size.
170///
171/// Supports the following formats:
172/// - `bytes=0-499` - First 500 bytes
173/// - `bytes=500-999` - Bytes 500-999
174/// - `bytes=500-` - From byte 500 to end
175/// - `bytes=-500` - Last 500 bytes
176///
177/// # Errors
178///
179/// Returns an error if:
180/// - The syntax is invalid
181/// - The unit is not "bytes"
182/// - Multiple ranges are specified (not yet supported)
183/// - The range is not satisfiable for the given resource size
184///
185/// # Examples
186///
187/// ```
188/// use fastapi_http::range::parse_range_header;
189///
190/// // First 500 bytes of a 1000-byte resource
191/// let range = parse_range_header("bytes=0-499", 1000).unwrap();
192/// assert_eq!(range.start, 0);
193/// assert_eq!(range.end, 499);
194/// assert_eq!(range.len(), 500);
195///
196/// // Last 100 bytes
197/// let range = parse_range_header("bytes=-100", 1000).unwrap();
198/// assert_eq!(range.start, 900);
199/// assert_eq!(range.end, 999);
200///
201/// // From byte 500 to end
202/// let range = parse_range_header("bytes=500-", 1000).unwrap();
203/// assert_eq!(range.start, 500);
204/// assert_eq!(range.end, 999);
205/// ```
206pub fn parse_range_header(header: &str, resource_size: u64) -> Result<ByteRange, RangeError> {
207    let spec = parse_range_spec(header)?;
208    spec.resolve(resource_size)
209}
210
211/// Parse a Range header into a `RangeSpec` without validating against resource size.
212///
213/// This is useful when you want to parse the header before knowing the resource size.
214///
215/// # Errors
216///
217/// Returns an error if the syntax is invalid or multiple ranges are specified.
218pub fn parse_range_spec(header: &str) -> Result<RangeSpec, RangeError> {
219    let header = header.trim();
220
221    // Split on '='
222    let (unit, range_set) = header
223        .split_once('=')
224        .ok_or_else(|| RangeError::InvalidSyntax("missing '=' separator".to_string()))?;
225
226    let unit = unit.trim();
227    let range_set = range_set.trim();
228
229    // Only support "bytes" unit
230    if !unit.eq_ignore_ascii_case("bytes") {
231        return Err(RangeError::UnsupportedUnit(unit.to_string()));
232    }
233
234    // Check for multiple ranges (comma-separated)
235    if range_set.contains(',') {
236        return Err(RangeError::MultipleRangesNotSupported);
237    }
238
239    // Parse single range: start-end, start-, or -suffix
240    parse_single_range(range_set)
241}
242
243/// Parse a single range specification (without the unit prefix).
244fn parse_single_range(range: &str) -> Result<RangeSpec, RangeError> {
245    let range = range.trim();
246
247    if range.is_empty() {
248        return Err(RangeError::InvalidSyntax("empty range".to_string()));
249    }
250
251    // Check for suffix range: -500
252    if range.starts_with('-') {
253        let suffix = &range[1..];
254        let length: u64 = suffix
255            .parse()
256            .map_err(|_| RangeError::InvalidSyntax(format!("invalid suffix length: {suffix}")))?;
257        return Ok(RangeSpec::Suffix { length });
258    }
259
260    // Split on '-'
261    let (start_str, end_str) = range
262        .split_once('-')
263        .ok_or_else(|| RangeError::InvalidSyntax("missing '-' separator".to_string()))?;
264
265    let start: u64 = start_str
266        .trim()
267        .parse()
268        .map_err(|_| RangeError::InvalidSyntax(format!("invalid start: {start_str}")))?;
269
270    let end_str = end_str.trim();
271
272    if end_str.is_empty() {
273        // Open-ended: bytes=500-
274        Ok(RangeSpec::From { start })
275    } else {
276        // Bounded: bytes=0-499
277        let end: u64 = end_str
278            .parse()
279            .map_err(|_| RangeError::InvalidSyntax(format!("invalid end: {end_str}")))?;
280        Ok(RangeSpec::FromTo { start, end })
281    }
282}
283
284/// Check if a request supports range requests based on Accept-Ranges.
285///
286/// Returns `true` if the resource can serve partial content.
287#[must_use]
288pub fn supports_ranges(accept_ranges: Option<&str>) -> bool {
289    match accept_ranges {
290        Some(value) => !value.eq_ignore_ascii_case("none"),
291        None => false,
292    }
293}
294
295/// Generate the Accept-Ranges header value for byte range support.
296#[must_use]
297pub const fn accept_ranges_bytes() -> &'static str {
298    "bytes"
299}
300
301/// Generate a Content-Range header for an unsatisfiable range.
302///
303/// Returns a string like "bytes */1000" for a 416 response.
304#[must_use]
305pub fn content_range_unsatisfiable(resource_size: u64) -> String {
306    format!("bytes */{resource_size}")
307}
308
309/// Result of validating an If-Range precondition.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum IfRangeResult {
312    /// The condition passed - serve partial content.
313    ServePartial,
314    /// The condition failed - serve full content (ignore Range header).
315    ServeFull,
316}
317
318/// Check an If-Range precondition header against a validator.
319///
320/// The If-Range header contains either:
321/// - An ETag value (e.g., `"abc123"`)
322/// - A Last-Modified date (e.g., `Wed, 21 Oct 2015 07:28:00 GMT`)
323///
324/// If the If-Range value matches the current resource, the Range request
325/// should be honored (return 206 Partial Content). Otherwise, the full
326/// resource should be returned (ignore the Range header, return 200 OK).
327///
328/// # Arguments
329///
330/// * `if_range` - The If-Range header value from the request
331/// * `etag` - The current ETag of the resource (if available)
332/// * `last_modified` - The current Last-Modified of the resource (if available)
333///
334/// # Returns
335///
336/// - `IfRangeResult::ServePartial` if the condition is satisfied
337/// - `IfRangeResult::ServeFull` if the condition fails or no validators are available
338///
339/// # Example
340///
341/// ```
342/// use fastapi_http::range::{check_if_range, IfRangeResult};
343///
344/// // ETag match
345/// let result = check_if_range(
346///     "\"abc123\"",
347///     Some("\"abc123\""),
348///     None,
349/// );
350/// assert_eq!(result, IfRangeResult::ServePartial);
351///
352/// // ETag mismatch
353/// let result = check_if_range(
354///     "\"abc123\"",
355///     Some("\"def456\""),
356///     None,
357/// );
358/// assert_eq!(result, IfRangeResult::ServeFull);
359/// ```
360#[must_use]
361pub fn check_if_range(
362    if_range: &str,
363    etag: Option<&str>,
364    last_modified: Option<&str>,
365) -> IfRangeResult {
366    let if_range = if_range.trim();
367
368    // Empty If-Range means the condition is satisfied
369    if if_range.is_empty() {
370        return IfRangeResult::ServePartial;
371    }
372
373    // Check if it looks like an ETag (starts with " or W/)
374    if if_range.starts_with('"') || if_range.starts_with("W/") {
375        // Compare as ETag
376        if let Some(current_etag) = etag {
377            // Strong comparison for If-Range (weak ETags don't match)
378            if etag_strong_match(if_range, current_etag) {
379                return IfRangeResult::ServePartial;
380            }
381        }
382        IfRangeResult::ServeFull
383    } else {
384        // Assume it's a date, compare as Last-Modified
385        if let Some(current_last_modified) = last_modified {
386            // Simple string comparison (dates should be in HTTP date format)
387            if if_range == current_last_modified {
388                return IfRangeResult::ServePartial;
389            }
390        }
391        IfRangeResult::ServeFull
392    }
393}
394
395/// Check if two ETags match using strong comparison.
396///
397/// For If-Range, weak ETags (W/"...") don't match. Only strong ETags match.
398fn etag_strong_match(etag1: &str, etag2: &str) -> bool {
399    // Weak ETags start with W/
400    if etag1.starts_with("W/") || etag2.starts_with("W/") {
401        return false;
402    }
403
404    // Both must be strong ETags and equal
405    etag1 == etag2
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    // =========================================================================
413    // ByteRange tests
414    // =========================================================================
415
416    #[test]
417    fn byte_range_new() {
418        let range = ByteRange::new(0, 499);
419        assert_eq!(range.start, 0);
420        assert_eq!(range.end, 499);
421    }
422
423    #[test]
424    fn byte_range_len() {
425        let range = ByteRange::new(0, 499);
426        assert_eq!(range.len(), 500);
427
428        let range = ByteRange::new(0, 0);
429        assert_eq!(range.len(), 1);
430
431        let range = ByteRange::new(100, 199);
432        assert_eq!(range.len(), 100);
433    }
434
435    #[test]
436    fn byte_range_content_range_header() {
437        let range = ByteRange::new(0, 499);
438        assert_eq!(range.content_range_header(1000), "bytes 0-499/1000");
439
440        let range = ByteRange::new(500, 999);
441        assert_eq!(range.content_range_header(1000), "bytes 500-999/1000");
442    }
443
444    #[test]
445    fn byte_range_display() {
446        let range = ByteRange::new(0, 499);
447        assert_eq!(format!("{range}"), "0-499");
448    }
449
450    #[test]
451    #[should_panic(expected = "start must be <= end")]
452    fn byte_range_invalid() {
453        let _ = ByteRange::new(500, 100);
454    }
455
456    // =========================================================================
457    // RangeSpec tests
458    // =========================================================================
459
460    #[test]
461    fn range_spec_from_to_valid() {
462        let spec = RangeSpec::FromTo { start: 0, end: 499 };
463        let range = spec.resolve(1000).unwrap();
464        assert_eq!(range.start, 0);
465        assert_eq!(range.end, 499);
466    }
467
468    #[test]
469    fn range_spec_from_to_clamped() {
470        // End exceeds resource size, should be clamped
471        let spec = RangeSpec::FromTo {
472            start: 0,
473            end: 9999,
474        };
475        let range = spec.resolve(1000).unwrap();
476        assert_eq!(range.start, 0);
477        assert_eq!(range.end, 999);
478    }
479
480    #[test]
481    fn range_spec_from_to_not_satisfiable() {
482        let spec = RangeSpec::FromTo {
483            start: 1000,
484            end: 1500,
485        };
486        let err = spec.resolve(1000).unwrap_err();
487        assert_eq!(
488            err,
489            RangeError::NotSatisfiable {
490                resource_size: 1000
491            }
492        );
493    }
494
495    #[test]
496    fn range_spec_from_valid() {
497        let spec = RangeSpec::From { start: 500 };
498        let range = spec.resolve(1000).unwrap();
499        assert_eq!(range.start, 500);
500        assert_eq!(range.end, 999);
501    }
502
503    #[test]
504    fn range_spec_from_not_satisfiable() {
505        let spec = RangeSpec::From { start: 1000 };
506        let err = spec.resolve(1000).unwrap_err();
507        assert_eq!(
508            err,
509            RangeError::NotSatisfiable {
510                resource_size: 1000
511            }
512        );
513    }
514
515    #[test]
516    fn range_spec_suffix_valid() {
517        let spec = RangeSpec::Suffix { length: 100 };
518        let range = spec.resolve(1000).unwrap();
519        assert_eq!(range.start, 900);
520        assert_eq!(range.end, 999);
521    }
522
523    #[test]
524    fn range_spec_suffix_exceeds_size() {
525        // Suffix larger than resource, returns entire resource
526        let spec = RangeSpec::Suffix { length: 2000 };
527        let range = spec.resolve(1000).unwrap();
528        assert_eq!(range.start, 0);
529        assert_eq!(range.end, 999);
530    }
531
532    #[test]
533    fn range_spec_suffix_zero() {
534        let spec = RangeSpec::Suffix { length: 0 };
535        let err = spec.resolve(1000).unwrap_err();
536        assert_eq!(
537            err,
538            RangeError::NotSatisfiable {
539                resource_size: 1000
540            }
541        );
542    }
543
544    #[test]
545    fn range_spec_empty_resource() {
546        let spec = RangeSpec::From { start: 0 };
547        let err = spec.resolve(0).unwrap_err();
548        assert_eq!(err, RangeError::NotSatisfiable { resource_size: 0 });
549    }
550
551    // =========================================================================
552    // parse_range_header tests
553    // =========================================================================
554
555    #[test]
556    fn parse_range_from_to() {
557        let range = parse_range_header("bytes=0-499", 1000).unwrap();
558        assert_eq!(range.start, 0);
559        assert_eq!(range.end, 499);
560        assert_eq!(range.len(), 500);
561    }
562
563    #[test]
564    fn parse_range_from() {
565        let range = parse_range_header("bytes=500-", 1000).unwrap();
566        assert_eq!(range.start, 500);
567        assert_eq!(range.end, 999);
568    }
569
570    #[test]
571    fn parse_range_suffix() {
572        let range = parse_range_header("bytes=-100", 1000).unwrap();
573        assert_eq!(range.start, 900);
574        assert_eq!(range.end, 999);
575    }
576
577    #[test]
578    fn parse_range_with_spaces() {
579        let range = parse_range_header("  bytes = 0 - 499  ", 1000).unwrap();
580        assert_eq!(range.start, 0);
581        assert_eq!(range.end, 499);
582    }
583
584    #[test]
585    fn parse_range_invalid_unit() {
586        let err = parse_range_header("items=0-10", 100).unwrap_err();
587        assert!(matches!(err, RangeError::UnsupportedUnit(_)));
588    }
589
590    #[test]
591    fn parse_range_multiple_not_supported() {
592        let err = parse_range_header("bytes=0-10, 20-30", 100).unwrap_err();
593        assert_eq!(err, RangeError::MultipleRangesNotSupported);
594    }
595
596    #[test]
597    fn parse_range_invalid_syntax_no_equals() {
598        let err = parse_range_header("bytes 0-10", 100).unwrap_err();
599        assert!(matches!(err, RangeError::InvalidSyntax(_)));
600    }
601
602    #[test]
603    fn parse_range_invalid_syntax_no_dash() {
604        let err = parse_range_header("bytes=100", 100).unwrap_err();
605        assert!(matches!(err, RangeError::InvalidSyntax(_)));
606    }
607
608    #[test]
609    fn parse_range_invalid_start() {
610        let err = parse_range_header("bytes=abc-100", 1000).unwrap_err();
611        assert!(matches!(err, RangeError::InvalidSyntax(_)));
612    }
613
614    #[test]
615    fn parse_range_invalid_end() {
616        let err = parse_range_header("bytes=0-xyz", 1000).unwrap_err();
617        assert!(matches!(err, RangeError::InvalidSyntax(_)));
618    }
619
620    #[test]
621    fn parse_range_not_satisfiable() {
622        let err = parse_range_header("bytes=1000-2000", 500).unwrap_err();
623        assert_eq!(err, RangeError::NotSatisfiable { resource_size: 500 });
624    }
625
626    // =========================================================================
627    // Helper function tests
628    // =========================================================================
629
630    #[test]
631    fn test_accept_ranges_bytes() {
632        assert_eq!(accept_ranges_bytes(), "bytes");
633    }
634
635    #[test]
636    fn test_content_range_unsatisfiable() {
637        assert_eq!(content_range_unsatisfiable(1000), "bytes */1000");
638    }
639
640    #[test]
641    fn test_supports_ranges() {
642        assert!(supports_ranges(Some("bytes")));
643        assert!(supports_ranges(Some("Bytes")));
644        assert!(!supports_ranges(Some("none")));
645        assert!(!supports_ranges(Some("None")));
646        assert!(!supports_ranges(None));
647    }
648
649    // =========================================================================
650    // RangeError Display tests
651    // =========================================================================
652
653    #[test]
654    fn range_error_display() {
655        let err = RangeError::InvalidSyntax("test".to_string());
656        assert!(format!("{err}").contains("invalid range syntax"));
657
658        let err = RangeError::UnsupportedUnit("items".to_string());
659        assert!(format!("{err}").contains("unsupported range unit: items"));
660
661        let err = RangeError::NotSatisfiable { resource_size: 500 };
662        assert!(format!("{err}").contains("range not satisfiable"));
663
664        let err = RangeError::MultipleRangesNotSupported;
665        assert!(format!("{err}").contains("multiple ranges not supported"));
666    }
667
668    // =========================================================================
669    // If-Range tests
670    // =========================================================================
671
672    #[test]
673    fn if_range_etag_match() {
674        let result = check_if_range("\"abc123\"", Some("\"abc123\""), None);
675        assert_eq!(result, IfRangeResult::ServePartial);
676    }
677
678    #[test]
679    fn if_range_etag_mismatch() {
680        let result = check_if_range("\"abc123\"", Some("\"def456\""), None);
681        assert_eq!(result, IfRangeResult::ServeFull);
682    }
683
684    #[test]
685    fn if_range_etag_no_current() {
686        let result = check_if_range("\"abc123\"", None, None);
687        assert_eq!(result, IfRangeResult::ServeFull);
688    }
689
690    #[test]
691    fn if_range_weak_etag_never_matches() {
692        // Weak ETag in If-Range
693        let result = check_if_range("W/\"abc123\"", Some("W/\"abc123\""), None);
694        assert_eq!(result, IfRangeResult::ServeFull);
695
696        // Weak ETag in current
697        let result = check_if_range("\"abc123\"", Some("W/\"abc123\""), None);
698        assert_eq!(result, IfRangeResult::ServeFull);
699    }
700
701    #[test]
702    fn if_range_date_match() {
703        let date = "Wed, 21 Oct 2015 07:28:00 GMT";
704        let result = check_if_range(date, None, Some(date));
705        assert_eq!(result, IfRangeResult::ServePartial);
706    }
707
708    #[test]
709    fn if_range_date_mismatch() {
710        let result = check_if_range(
711            "Wed, 21 Oct 2015 07:28:00 GMT",
712            None,
713            Some("Thu, 22 Oct 2015 07:28:00 GMT"),
714        );
715        assert_eq!(result, IfRangeResult::ServeFull);
716    }
717
718    #[test]
719    fn if_range_date_no_current() {
720        let result = check_if_range("Wed, 21 Oct 2015 07:28:00 GMT", None, None);
721        assert_eq!(result, IfRangeResult::ServeFull);
722    }
723
724    #[test]
725    fn if_range_empty_header() {
726        let result = check_if_range("", None, None);
727        assert_eq!(result, IfRangeResult::ServePartial);
728    }
729
730    #[test]
731    fn if_range_whitespace_trimmed() {
732        let result = check_if_range("  \"abc123\"  ", Some("\"abc123\""), None);
733        assert_eq!(result, IfRangeResult::ServePartial);
734    }
735}