Skip to main content

range_requests/headers/
if_range.rs

1#[cfg(feature = "axum")]
2use std::convert::Infallible;
3use std::str::FromStr;
4
5use http::HeaderValue;
6
7use crate::headers::range::HttpRange;
8
9/// A typed HTTP `If-Range` header.
10///
11/// Per [RFC 9110 Section 13.1.5], `If-Range` can contain either an HTTP-date
12/// or an entity-tag. When present alongside a `Range` header, the server must
13/// evaluate the validator against the current representation:
14///
15/// - If the validator **matches**, the `Range` is honored (206 Partial Content).
16/// - If the validator **does not match**, the `Range` is ignored and the full
17///   representation is served (200 OK).
18///
19/// [RFC 9110 Section 13.1.5]: https://www.rfc-editor.org/rfc/rfc9110#section-13.1.5
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum IfRange {
22    /// An HTTP-date validator (the raw header value, to be compared with `Last-Modified`).
23    Date(HeaderValue),
24    /// An entity-tag validator (the raw header value, to be compared with `ETag`).
25    ETag(HeaderValue),
26}
27
28impl IfRange {
29    /// Evaluates the `If-Range` condition and returns the [`HttpRange`] only if
30    /// the condition holds.
31    ///
32    /// - `range`: the parsed `Range` header value.
33    /// - `last_modified`: the current `Last-Modified` header of the representation.
34    /// - `etag`: the current `ETag` header of the representation.
35    ///
36    /// Returns `Some(range)` if the validator matches (the range should be
37    /// honored), or `None` if it does not (the full representation should be
38    /// served).
39    ///
40    /// Per [RFC 9110 Section 13.1.5], the comparison uses the raw header values:
41    /// - For dates, the `If-Range` value must be an **exact byte-for-byte match**
42    ///   of the `Last-Modified` header value.
43    /// - For entity-tags, the `If-Range` value must be a **strong comparison**
44    ///   match against the `ETag` header value. Weak entity-tags never match.
45    ///
46    /// [RFC 9110 Section 13.1.5]: https://www.rfc-editor.org/rfc/rfc9110#section-13.1.5
47    pub fn evaluate(
48        &self,
49        range: HttpRange,
50        last_modified: Option<&HeaderValue>,
51        etag: Option<&HeaderValue>,
52    ) -> Option<HttpRange> {
53        let matches = match self {
54            IfRange::Date(date) => last_modified.is_some_and(|lm| lm == date),
55            IfRange::ETag(tag) => etag.is_some_and(|et| strong_etag_eq(tag, et)),
56        };
57
58        if matches { Some(range) } else { None }
59    }
60}
61
62/// Performs a strong comparison of two entity-tags.
63///
64/// Per [RFC 9110 Section 8.8.3.2], two entity-tags are strongly equivalent if
65/// both are **not** weak and their opaque-tags match character by character.
66///
67/// [RFC 9110 Section 8.8.3.2]: https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3.2
68fn strong_etag_eq(a: &HeaderValue, b: &HeaderValue) -> bool {
69    let a = a.as_bytes();
70    let b = b.as_bytes();
71
72    // Weak tags (W/"...") never match in a strong comparison
73    if a.starts_with(b"W/") || b.starts_with(b"W/") {
74        return false;
75    }
76
77    a == b
78}
79
80impl FromStr for IfRange {
81    type Err = InvalidIfRange;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        let s = s.trim();
85        if s.is_empty() {
86            return Err(InvalidIfRange);
87        }
88
89        let value = HeaderValue::from_str(s).map_err(|_| InvalidIfRange)?;
90
91        // Per RFC 9110 Section 13.1.5, the field value is either an entity-tag
92        // or an HTTP-date. Entity-tags start with `"` or `W/"`.
93        if s.starts_with('"') || s.starts_with("W/\"") {
94            Ok(IfRange::ETag(value))
95        } else {
96            Ok(IfRange::Date(value))
97        }
98    }
99}
100
101impl TryFrom<&HeaderValue> for IfRange {
102    type Error = InvalidIfRange;
103
104    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
105        value.to_str().map_err(|_| InvalidIfRange)?.parse::<Self>()
106    }
107}
108
109/// An error returned when parsing an `If-Range` header fails.
110#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
111#[error("Invalid If-Range header")]
112pub struct InvalidIfRange;
113
114#[cfg(feature = "axum")]
115impl<S> axum_core::extract::OptionalFromRequestParts<S> for IfRange
116where
117    S: Send + Sync,
118{
119    type Rejection = Infallible;
120
121    async fn from_request_parts(
122        parts: &mut http::request::Parts,
123        _state: &S,
124    ) -> Result<Option<Self>, Self::Rejection> {
125        let if_range = parts
126            .headers
127            .get(http::header::IF_RANGE)
128            .and_then(|v| IfRange::try_from(v).ok());
129        Ok(if_range)
130    }
131}