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}