actix_web_lab/
cache_control.rs

1//! Cache-Control typed header.
2//!
3//! See [`CacheControl`] docs.
4
5use std::{fmt, str};
6
7use actix_http::{
8    error::ParseError,
9    header::{
10        fmt_comma_delimited, from_comma_delimited, Header, HeaderName, HeaderValue,
11        InvalidHeaderValue, TryIntoHeaderValue,
12    },
13    HttpMessage,
14};
15use actix_web::http::header;
16use derive_more::{Deref, DerefMut};
17
18/// The `Cache-Control` header, defined in [RFC 7234 §5.2].
19///
20/// Includes built-in support for directives introduced in subsequent specifications. [Read more
21/// about the full list of supported directives on MDN][mdn].
22///
23/// The `Cache-Control` header field is used to specify [directives](CacheDirective) for caches
24/// along the request/response chain. Such cache directives are unidirectional in that the presence
25/// of a directive in a request does not imply that the same directive is to be given in the
26/// response.
27///
28/// # ABNF
29/// ```text
30/// Cache-Control   = 1#cache-directive
31/// cache-directive = token [ \"=\" ( token / quoted-string ) ]
32/// ```
33///
34/// # Example Values
35/// - `max-age=30`
36/// - `no-cache, no-store`
37/// - `public, max-age=604800, immutable`
38/// - `private, community=\"UCI\"`
39///
40/// # Examples
41/// ```
42/// use actix_web::{
43///     http::header::{CacheControl, CacheDirective},
44///     HttpResponse,
45/// };
46///
47/// let mut builder = HttpResponse::Ok();
48/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
49/// ```
50///
51/// ```
52/// use actix_web::{
53///     http::header::{CacheControl, CacheDirective},
54///     HttpResponse,
55/// };
56///
57/// let mut builder = HttpResponse::Ok();
58/// builder.insert_header(CacheControl(vec![
59///     CacheDirective::NoCache,
60///     CacheDirective::Private,
61///     CacheDirective::MaxAge(360u32),
62///     CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())),
63/// ]));
64/// ```
65///
66/// [RFC 7234 §5.2]: https://datatracker.ietf.org/doc/html/rfc7234#section-5.2
67/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
68#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
69pub struct CacheControl(pub Vec<CacheDirective>);
70
71impl fmt::Display for CacheControl {
72    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
73        fmt_comma_delimited(f, &self.0[..])
74    }
75}
76
77impl TryIntoHeaderValue for CacheControl {
78    type Error = InvalidHeaderValue;
79
80    fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
81        HeaderValue::try_from(self.to_string())
82    }
83}
84
85impl Header for CacheControl {
86    fn name() -> HeaderName {
87        header::CACHE_CONTROL
88    }
89
90    fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError> {
91        let headers = msg.headers().get_all(Self::name());
92        from_comma_delimited(headers).and_then(|items| {
93            if items.is_empty() {
94                Err(ParseError::Header)
95            } else {
96                Ok(CacheControl(items))
97            }
98        })
99    }
100}
101
102/// Directives contained in a [`CacheControl`] header.
103///
104/// [Read more on MDN.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#cache_directives)
105#[derive(Debug, Clone, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum CacheDirective {
108    /// The `max-age=N` directive.
109    ///
110    /// When used as a request directive, it indicates that the client allows a stored response that
111    /// is generated on the origin server within N seconds — where `N` may be any non-negative
112    /// integer (including 0). [Read more on MDN.][mdn_req]
113    ///
114    /// When used as a response directive, it indicates that the response remains fresh until `N`
115    /// seconds after the response is generated. [Read more on MDN.][mdn_res]
116    ///
117    /// [mdn_req]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#max-age_2
118    /// [mdn_res]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#max-age
119    MaxAge(u32),
120
121    /// The `max-stale=N` request directive.
122    ///
123    /// This directive indicates that the client allows a stored response that is stale within `N`
124    /// seconds. [Read more on MDN.][mdn]
125    ///
126    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#max-stale
127    MaxStale(u32),
128
129    /// The `min-fresh=N` request directive.
130    ///
131    /// This directive indicates that the client allows a stored response that is fresh for at least
132    /// `N` seconds. [Read more on MDN.][mdn]
133    ///
134    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
135    MinFresh(u32),
136
137    /// The `s-maxage=N` response directive.
138    ///
139    /// This directive also indicates how long the response is fresh for (similar to `max-age`)—but
140    /// it is specific to shared caches, and they will ignore `max-age` when it is present. [Read
141    /// more on MDN.][mdn]
142    ///
143    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#s-maxage
144    SMaxAge(u32),
145
146    /// The `no-cache` directive.
147    ///
148    /// When used as a request directive, it asks caches to validate the response with the origin
149    /// server before reuse. [Read more on MDN.][mdn_req]
150    ///
151    /// When used as a response directive, it indicates that the response can be stored in caches,
152    /// but the response must be validated with the origin server before each reuse, even when the
153    /// cache is disconnected from the origin server. [Read more on MDN.][mdn_res]
154    ///
155    /// [mdn_req]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-cache_2
156    /// [mdn_res]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-cache
157    NoCache,
158
159    /// The `no-store` directive.
160    ///
161    /// When used as a request directive, it allows a client to request that caches refrain from
162    /// storing the request and corresponding response — even if the origin server's response could
163    /// be stored. [Read more on MDN.][mdn_req]
164    ///
165    /// When used as a response directive, it indicates that the response can be stored in caches,
166    /// but the response must be validated with the origin server before each reuse, even when the
167    /// cache is disconnected from the origin server. [Read more on MDN.][mdn_res]
168    ///
169    /// [mdn_req]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-store_2
170    /// [mdn_res]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-store
171    NoStore,
172
173    /// The `no-transform` directive.
174    ///
175    /// This directive, in both request and response contexts, indicates that any intermediary
176    /// (regardless of whether it implements a cache) shouldn't transform the response contents.
177    /// [Read more on MDN.][mdn]
178    ///
179    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-transform
180    NoTransform,
181
182    /// The `only-if-cached` request directive.
183    ///
184    /// This directive indicates that caches should obtain an already-cached response. If a cache
185    /// has stored a response, it's reused. [Read more on MDN.][mdn]
186    ///
187    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#only-if-cached
188    OnlyIfCached,
189
190    /// The `must-revalidate` response directive.
191    ///
192    /// This directive indicates that the response can be stored in caches and can be reused while
193    /// fresh. If the response becomes stale, it must be validated with the origin server before
194    /// reuse. [Read more on MDN.][mdn]
195    ///
196    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#must-revalidate
197    MustRevalidate,
198
199    /// The `proxy-revalidate` response directive.
200    ///
201    /// This directive is the equivalent of must-revalidate, but specifically for shared caches
202    /// only. [Read more on MDN.][mdn]
203    ///
204    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#proxy-revalidate
205    ProxyRevalidate,
206
207    /// The `must-understand` response directive.
208    ///
209    /// This directive indicates that a cache should store the response only if it understands the
210    /// requirements for caching based on status code. [Read more on MDN.][mdn]
211    ///
212    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#must-understand
213    MustUnderstand,
214
215    /// The `private` response directive.
216    ///
217    /// This directive indicates that the response can be stored only in a private cache (e.g. local
218    /// caches in browsers). [Read more on MDN.][mdn]
219    ///
220    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#private
221    Private,
222
223    /// The `public` response directive.
224    ///
225    /// This directive indicates that the response can be stored in a shared cache. Responses for
226    /// requests with `Authorization` header fields must not be stored in a shared cache; however,
227    /// the `public` directive will cause such responses to be stored in a shared cache. [Read more
228    /// on MDN.][mdn]
229    ///
230    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#public
231    Public,
232
233    /// The `immutable` response directive.
234    ///
235    /// This directive indicates that the response will not be updated while it's fresh. [Read more
236    /// on MDN.][mdn]
237    ///
238    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
239    Immutable,
240
241    /// The `stale-while-revalidate` response directive.
242    ///
243    /// This directive indicates that the cache could reuse a stale response while it revalidates it
244    /// to a cache. [Read more on MDN.][mdn]
245    ///
246    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate
247    StaleWhileRevalidate,
248
249    /// The `stale-if-error` directive.
250    ///
251    /// When used as a response directive, it indicates that the cache can reuse a stale response
252    /// when an origin server responds with an error (500, 502, 503, or 504). [Read more on MDN.][mdn_res]
253    ///
254    /// [mdn_res]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error
255    StaleIfError,
256
257    /// Extension directive.
258    ///
259    /// An unknown directives is collected into this variant with an optional argument value.
260    Extension(String, Option<String>),
261}
262
263impl fmt::Display for CacheDirective {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        use self::CacheDirective::*;
266
267        let dir_str = match self {
268            MaxAge(secs) => return write!(f, "max-age={secs}"),
269            MaxStale(secs) => return write!(f, "max-stale={secs}"),
270            MinFresh(secs) => return write!(f, "min-fresh={secs}"),
271            SMaxAge(secs) => return write!(f, "s-maxage={secs}"),
272
273            NoCache => "no-cache",
274            NoStore => "no-store",
275            NoTransform => "no-transform",
276            OnlyIfCached => "only-if-cached",
277
278            MustRevalidate => "must-revalidate",
279            ProxyRevalidate => "proxy-revalidate",
280            MustUnderstand => "must-understand",
281            Private => "private",
282            Public => "public",
283
284            Immutable => "immutable",
285            StaleWhileRevalidate => "stale-while-revalidate",
286            StaleIfError => "stale-if-error",
287
288            Extension(name, None) => name.as_str(),
289            Extension(name, Some(arg)) => return write!(f, "{name}={arg}"),
290        };
291
292        f.write_str(dir_str)
293    }
294}
295
296impl str::FromStr for CacheDirective {
297    type Err = Option<<u32 as str::FromStr>::Err>;
298
299    fn from_str(dir: &str) -> Result<Self, Self::Err> {
300        use CacheDirective::*;
301
302        match dir {
303            "" => Err(None),
304
305            "no-cache" => Ok(NoCache),
306            "no-store" => Ok(NoStore),
307            "no-transform" => Ok(NoTransform),
308            "only-if-cached" => Ok(OnlyIfCached),
309            "must-revalidate" => Ok(MustRevalidate),
310            "public" => Ok(Public),
311            "private" => Ok(Private),
312            "proxy-revalidate" => Ok(ProxyRevalidate),
313            "must-understand" => Ok(MustUnderstand),
314
315            "immutable" => Ok(Immutable),
316            "stale-while-revalidate" => Ok(StaleWhileRevalidate),
317            "stale-if-error" => Ok(StaleIfError),
318
319            _ => match dir
320                .split_once('=')
321                .map(|(dir, arg)| (dir, arg.trim_matches('"')))
322            {
323                // empty argument is not allowed
324                Some((_dir, "")) => Err(None),
325
326                Some(("max-age", secs)) => secs.parse().map(MaxAge).map_err(Some),
327                Some(("max-stale", secs)) => secs.parse().map(MaxStale).map_err(Some),
328                Some(("min-fresh", secs)) => secs.parse().map(MinFresh).map_err(Some),
329                Some(("s-maxage", secs)) => secs.parse().map(SMaxAge).map_err(Some),
330
331                // unknown but correctly formatted directive+argument
332                Some((left, right)) => Ok(Extension(left.to_owned(), Some(right.to_owned()))),
333
334                // unknown directive
335                None => Ok(Extension(dir.to_owned(), None)),
336            },
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn deref() {
347        let mut cache_ctrl = CacheControl(vec![]);
348        let _: &[CacheDirective] = &cache_ctrl;
349        let _: &mut [CacheDirective] = &mut cache_ctrl;
350    }
351}
352
353#[cfg(test)]
354crate::test::header_test_module! {
355    CacheControl,
356    test_parse_and_format {
357        header_round_trip_test!(no_headers, [b""; 0], None);
358        header_round_trip_test!(empty_header, [b""; 1], None);
359        header_round_trip_test!(bad_syntax, [b"foo="], None);
360
361        header_round_trip_test!(
362            multiple_headers,
363            [&b"no-cache"[..], &b"private"[..]],
364            Some(CacheControl(vec![
365                CacheDirective::NoCache,
366                CacheDirective::Private,
367            ]))
368        );
369
370        header_round_trip_test!(
371            argument,
372            [b"max-age=100, private"],
373            Some(CacheControl(vec![
374                CacheDirective::MaxAge(100),
375                CacheDirective::Private,
376            ]))
377        );
378
379        header_round_trip_test!(
380            immutable,
381            [b"public, max-age=604800, immutable"],
382            Some(CacheControl(vec![
383                CacheDirective::Public,
384                CacheDirective::MaxAge(604800),
385                CacheDirective::Immutable,
386            ]))
387        );
388
389        header_round_trip_test!(
390            stale_if_while,
391            [b"must-understand, stale-while-revalidate, stale-if-error"],
392            Some(CacheControl(vec![
393                CacheDirective::MustUnderstand,
394                CacheDirective::StaleWhileRevalidate,
395                CacheDirective::StaleIfError,
396            ]))
397        );
398
399        header_round_trip_test!(
400            extension,
401            [b"foo, bar=baz"],
402            Some(CacheControl(vec![
403                CacheDirective::Extension("foo".to_owned(), None),
404                CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())),
405            ]))
406        );
407
408        #[test]
409        fn parse_quote_form() {
410            let req = test::TestRequest::default()
411                .insert_header((header::CACHE_CONTROL, "max-age=\"200\""))
412                .finish();
413
414            assert_eq!(
415                Header::parse(&req).ok(),
416                Some(CacheControl(vec![CacheDirective::MaxAge(200)]))
417            )
418        }
419
420        #[test]
421        fn trailing_equals_fails() {
422            let req = test_request!(GET "/"; "cache-control" => "extension=").to_request();
423            CacheControl::parse(&req).unwrap_err();
424        }
425    }
426}