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}