Skip to main content

rama_http_headers/common/
vary.rs

1use rama_core::error::BoxError;
2use rama_core::telemetry::tracing;
3use rama_http_types::{HeaderName, HeaderValue, header};
4use rama_utils::collections::{NonEmptyVec, non_empty_vec};
5
6use crate::ClientHint;
7use crate::util::{
8    FlatCsvSeparator, TryFromValues, try_decode_flat_csv_header_values_as_non_empty_vec,
9    try_encode_non_empty_vec_as_flat_csv_header_value,
10};
11
12/// `Vary` header, defined in [RFC7231](https://tools.ietf.org/html/rfc7231#section-7.1.4)
13///
14/// The "Vary" header field in a response describes what parts of a
15/// request message, aside from the method, Host header field, and
16/// request target, might influence the origin server's process for
17/// selecting and representing this response.  The value consists of
18/// either a single asterisk ("*") or a list of header field names
19/// (case-insensitive).
20///
21/// # ABNF
22///
23/// ```text
24/// Vary = "*" / 1#field-name
25/// ```
26///
27/// # Example values
28///
29/// * `accept-encoding, accept-language`
30///
31/// # Example
32///
33/// ```
34/// use rama_http_headers::Vary;
35///
36/// let vary = Vary::any();
37/// ```
38#[derive(Clone, Debug)]
39pub struct Vary(Directive);
40
41#[derive(Clone, Debug)]
42enum Directive {
43    Any,
44    Headers(NonEmptyVec<HeaderName>),
45}
46
47impl TryFrom<&Directive> for HeaderValue {
48    type Error = BoxError;
49
50    fn try_from(value: &Directive) -> Result<Self, Self::Error> {
51        match value {
52            Directive::Any => Ok(DIRECTIVE_HEADER_VALUE_ANY),
53            Directive::Headers(values) => {
54                try_encode_non_empty_vec_as_flat_csv_header_value(values, FlatCsvSeparator::Comma)
55            }
56        }
57    }
58}
59
60impl TryFromValues for Directive {
61    fn try_from_values<'i, I>(values: &mut I) -> Result<Self, crate::Error>
62    where
63        Self: Sized,
64        I: Iterator<Item = &'i HeaderValue>,
65    {
66        match try_decode_flat_csv_header_values_as_non_empty_vec(values, FlatCsvSeparator::Comma) {
67            Ok(values) => {
68                if values.len() == 1 && values.first() == "*" {
69                    Ok(Self::Any)
70                } else {
71                    Ok(Self::Headers(values))
72                }
73            }
74            Err(err) => {
75                tracing::trace!("invalid vary directive: {err}");
76                Err(crate::Error::invalid())
77            }
78        }
79    }
80}
81
82const DIRECTIVE_HEADER_VALUE_ANY: HeaderValue = HeaderValue::from_static("*");
83
84impl crate::TypedHeader for Vary {
85    fn name() -> &'static ::rama_http_types::header::HeaderName {
86        &::rama_http_types::header::VARY
87    }
88}
89
90impl crate::HeaderDecode for Vary {
91    fn decode<'i, I>(values: &mut I) -> Result<Self, crate::Error>
92    where
93        I: Iterator<Item = &'i ::rama_http_types::header::HeaderValue>,
94    {
95        Directive::try_from_values(values).map(Self)
96    }
97}
98
99impl crate::HeaderEncode for Vary {
100    fn encode<E: Extend<::rama_http_types::HeaderValue>>(&self, values: &mut E) {
101        match HeaderValue::try_from(&self.0) {
102            Ok(value) => values.extend(::std::iter::once(value)),
103            Err(err) => {
104                rama_core::telemetry::tracing::debug!(
105                    "failed to encode vary directive {:?} as flat csv header: {err}",
106                    self.0,
107                );
108            }
109        }
110    }
111}
112
113impl Vary {
114    /// Create a new `Vary: *` header.
115    #[must_use]
116    pub const fn any() -> Self {
117        Self(Directive::Any)
118    }
119
120    /// Creates a new `Vary` header with the request headers that may be involved in a CORS preflight request.
121    #[must_use]
122    #[inline(always)]
123    pub fn preflight_request_headers() -> Self {
124        Self::headers(non_empty_vec![
125            header::ORIGIN,
126            header::ACCESS_CONTROL_REQUEST_METHOD,
127            header::ACCESS_CONTROL_REQUEST_HEADERS,
128        ])
129    }
130
131    /// Create a new `Vary` header for the given header (CS) names.
132    #[must_use]
133    pub fn headers(values: NonEmptyVec<HeaderName>) -> Self {
134        Self(Directive::Headers(values))
135    }
136
137    /// Create a `Vary` listing every request-header spelling the given
138    /// [`ClientHint`]s are read under — the same `Sec-CH-*` + legacy bare names
139    /// an [`AcceptCh`](crate::AcceptCh) built from the same hints advertises —
140    /// so caches keep a separate representation per client-hint value. Returns
141    /// `None` only for an empty hint iterator.
142    #[must_use]
143    pub fn from_client_hints<'a>(hints: impl IntoIterator<Item = &'a ClientHint>) -> Option<Self> {
144        NonEmptyVec::collect(hints.into_iter().flat_map(ClientHint::iter_header_names))
145            .map(Self::headers)
146    }
147
148    /// Check if this includes `*`.
149    pub const fn is_any(&self) -> bool {
150        matches!(&self.0, Directive::Any)
151    }
152
153    /// Iterate the header names of this `Vary`
154    /// or `None` if it was an 'any' vary header.
155    pub fn iter_strs(&self) -> Option<impl Iterator<Item = &HeaderName>> {
156        match &self.0 {
157            Directive::Any => None,
158            Directive::Headers(non_empty_vec) => Some(non_empty_vec.iter()),
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::super::{test_decode, test_encode};
166    use super::*;
167    use rama_utils::collections::non_empty_vec;
168
169    #[test]
170    fn any_is_any() {
171        assert!(Vary::any().is_any());
172    }
173
174    #[test]
175    fn decode_header_single_open() {
176        let Vary(directive) = test_decode(&["foo, bar"]).unwrap();
177        match directive {
178            Directive::Any => panic!("unexpected any directive"),
179            Directive::Headers(non_empty_vec) => {
180                assert_eq!(2, non_empty_vec.len());
181                assert_eq!(non_empty_vec[0], "foo");
182                assert_eq!(non_empty_vec[1], "bar");
183            }
184        }
185    }
186
187    #[test]
188    fn decode_header_single_close() {
189        let Vary(directive) = test_decode(&["*"]).unwrap();
190        match directive {
191            Directive::Any => (),
192            Directive::Headers(non_empty_vec) => {
193                panic!("unexpected headers directive, headers: {non_empty_vec:?}")
194            }
195        }
196    }
197
198    #[test]
199    fn encode_headers() {
200        let vary = Vary::headers(non_empty_vec![
201            ::rama_http_types::header::USER_AGENT,
202            ::rama_http_types::header::CONTENT_ENCODING,
203        ]);
204
205        let headers = test_encode(vary);
206        assert_eq!(headers["vary"], "user-agent, content-encoding");
207    }
208
209    #[test]
210    fn from_client_hints_lists_every_spelling() {
211        let vary = Vary::from_client_hints(&[ClientHint::SaveData, ClientHint::Ect])
212            .expect("non-empty hint set");
213        let headers = test_encode(vary);
214        // both the Sec-CH-* and the legacy bare name of each hint, matching what
215        // `AcceptCh` advertises, so caches key on whichever the UA sends.
216        assert_eq!(
217            headers["vary"],
218            "sec-ch-save-data, save-data, sec-ch-ect, ect",
219        );
220    }
221
222    #[test]
223    fn from_client_hints_empty_is_none() {
224        assert!(Vary::from_client_hints(std::iter::empty::<&ClientHint>()).is_none());
225    }
226
227    #[test]
228    fn decode_with_empty_header_value() {
229        assert!(test_decode::<Vary>(&[""]).is_none());
230    }
231
232    #[test]
233    fn decode_with_no_headers() {
234        assert!(test_decode::<Vary>(&[]).is_none());
235    }
236
237    #[test]
238    fn decode_with_invalid_value_str() {
239        assert!(test_decode::<Vary>(&["foo foo, bar"]).is_none());
240    }
241}