Skip to main content

rama_http_headers/common/
if_range.rs

1use std::time::SystemTime;
2
3use rama_core::error::BoxError;
4use rama_core::telemetry::tracing;
5use rama_http_types::HeaderValue;
6
7use super::{ETag, LastModified};
8use crate::Error;
9use crate::util::{EntityTag, HttpDate, TryFromValues};
10
11/// `If-Range` header, defined in [RFC7233](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2)
12///
13/// If a client has a partial copy of a representation and wishes to have
14/// an up-to-date copy of the entire representation, it could use the
15/// Range header field with a conditional GET (using either or both of
16/// If-Unmodified-Since and If-Match.)  However, if the precondition
17/// fails because the representation has been modified, the client would
18/// then have to make a second request to obtain the entire current
19/// representation.
20///
21/// The `If-Range` header field allows a client to \"short-circuit\" the
22/// second request.  Informally, its meaning is as follows: if the
23/// representation is unchanged, send me the part(s) that I am requesting
24/// in Range; otherwise, send me the entire representation.
25///
26/// # ABNF
27///
28/// ```text
29/// If-Range = entity-tag / HTTP-date
30/// ```
31///
32/// # Example values
33///
34/// * `Sat, 29 Oct 1994 19:43:31 GMT`
35/// * `\"xyzzy\"`
36///
37/// # Examples
38///
39/// ```
40/// use rama_http_headers::IfRange;
41/// use std::time::Duration;
42/// use rama_utils::time::now_system_time;
43///
44/// let fetched = now_system_time() - Duration::from_hours(24);
45/// let if_range = IfRange::date(fetched);
46/// ```
47#[derive(Clone, Debug, PartialEq)]
48pub struct IfRange(IfRange_);
49
50impl crate::TypedHeader for IfRange {
51    fn name() -> &'static ::rama_http_types::header::HeaderName {
52        &::rama_http_types::header::IF_RANGE
53    }
54}
55
56impl crate::HeaderDecode for IfRange {
57    fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
58    where
59        I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
60    {
61        crate::util::TryFromValues::try_from_values(values).map(IfRange)
62    }
63}
64
65impl crate::HeaderEncode for IfRange {
66    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
67        match HeaderValue::try_from(&self.0) {
68            Ok(value) => values.extend(::std::iter::once(value)),
69            Err(err) => {
70                tracing::debug!("failed to encode if-range value as header: {err}");
71            }
72        }
73    }
74}
75
76impl IfRange {
77    /// Create an `IfRange` header with an entity tag.
78    pub fn etag(tag: ETag) -> Self {
79        Self(IfRange_::EntityTag(tag.0))
80    }
81
82    /// Create an `IfRange` header with a date value.
83    #[must_use]
84    pub fn date(time: SystemTime) -> Self {
85        Self(IfRange_::Date(time.into()))
86    }
87
88    /// Checks if the resource has been modified, or if the range request
89    /// can be served.
90    pub fn is_modified(&self, etag: Option<&ETag>, last_modified: Option<&LastModified>) -> bool {
91        match self.0 {
92            IfRange_::Date(since) => last_modified.map(|time| since < time.0).unwrap_or(true),
93            IfRange_::EntityTag(ref entity) => {
94                etag.map(|etag| !etag.0.strong_eq(entity)).unwrap_or(true)
95            }
96        }
97    }
98}
99
100#[derive(Clone, Debug, PartialEq)]
101enum IfRange_ {
102    /// The entity-tag the client has of the resource
103    EntityTag(EntityTag),
104    /// The date when the client retrieved the resource
105    Date(HttpDate),
106}
107
108impl TryFromValues for IfRange_ {
109    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
110    where
111        I: Iterator<Item = &'i HeaderValue>,
112    {
113        values
114            .next()
115            .and_then(|val| {
116                if let Some(tag) = EntityTag::from_val(val) {
117                    return Some(Self::EntityTag(tag));
118                }
119
120                let date = HttpDate::from_val(val)?;
121                Some(Self::Date(date))
122            })
123            .ok_or_else(Error::invalid)
124    }
125}
126
127impl<'a> TryFrom<&'a IfRange_> for HeaderValue {
128    type Error = BoxError;
129
130    fn try_from(if_range: &'a IfRange_) -> Result<Self, Self::Error> {
131        match *if_range {
132            IfRange_::EntityTag(ref tag) => Ok(tag.into()),
133            IfRange_::Date(ref date) => date.try_into(),
134        }
135    }
136}
137
138/*
139#[cfg(test)]
140mod tests {
141    use std::str;
142    use *;
143    use super::IfRange as HeaderField;
144    test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
145    test_header!(test2, vec![b"\"xyzzy\""]);
146    test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
147}
148*/
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_is_modified_etag() {
156        let etag = ETag::from_static("\"xyzzy\"");
157        let if_range = IfRange::etag(etag.clone());
158
159        assert!(!if_range.is_modified(Some(&etag), None));
160
161        let etag = ETag::from_static("W/\"xyzzy\"");
162        assert!(if_range.is_modified(Some(&etag), None));
163    }
164}