pingora_cache/
cache_control.rs

1// Copyright 2025 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Functions and utilities to help parse Cache-Control headers
16
17use super::*;
18
19use http::header::HeaderName;
20use http::HeaderValue;
21use indexmap::IndexMap;
22use once_cell::sync::Lazy;
23use pingora_error::{Error, ErrorType};
24use regex::bytes::Regex;
25use std::num::IntErrorKind;
26use std::slice;
27use std::str;
28
29/// The max delta-second per [RFC 9111](https://datatracker.ietf.org/doc/html/rfc9111#section-1.2.2)
30// "If a cache receives a delta-seconds
31// value greater than the greatest integer it can represent, or if any
32// of its subsequent calculations overflows, the cache MUST consider the
33// value to be either 2147483648 (2^31) or the greatest positive integer
34// it can conveniently represent."
35pub const DELTA_SECONDS_OVERFLOW_VALUE: u32 = 2147483648;
36
37/// Cache control directive key type
38pub type DirectiveKey = String;
39
40/// Cache control directive value type
41#[derive(Debug)]
42pub struct DirectiveValue(pub Vec<u8>);
43
44impl AsRef<[u8]> for DirectiveValue {
45    fn as_ref(&self) -> &[u8] {
46        &self.0
47    }
48}
49
50impl DirectiveValue {
51    /// A [DirectiveValue] without quotes (`"`).
52    pub fn parse_as_bytes(&self) -> &[u8] {
53        self.0
54            .strip_prefix(b"\"")
55            .and_then(|bytes| bytes.strip_suffix(b"\""))
56            .unwrap_or(&self.0[..])
57    }
58
59    /// A [DirectiveValue] without quotes (`"`) as `str`.
60    pub fn parse_as_str(&self) -> Result<&str> {
61        str::from_utf8(self.parse_as_bytes()).or_else(|e| {
62            Error::e_because(ErrorType::InternalError, "could not parse value as utf8", e)
63        })
64    }
65
66    /// Parse the [DirectiveValue] as delta seconds
67    ///
68    /// `"`s are ignored. The value is capped to [DELTA_SECONDS_OVERFLOW_VALUE].
69    pub fn parse_as_delta_seconds(&self) -> Result<u32> {
70        match self.parse_as_str()?.parse::<u32>() {
71            Ok(value) => Ok(value),
72            Err(e) => {
73                // delta-seconds expect to handle positive overflow gracefully
74                if e.kind() == &IntErrorKind::PosOverflow {
75                    Ok(DELTA_SECONDS_OVERFLOW_VALUE)
76                } else {
77                    Error::e_because(ErrorType::InternalError, "could not parse value as u32", e)
78                }
79            }
80        }
81    }
82}
83
84/// An ordered map to store cache control key value pairs.
85pub type DirectiveMap = IndexMap<DirectiveKey, Option<DirectiveValue>>;
86
87/// Parsed Cache-Control directives
88#[derive(Debug)]
89pub struct CacheControl {
90    /// The parsed directives
91    pub directives: DirectiveMap,
92}
93
94/// Cacheability calculated from cache control.
95#[derive(Debug, PartialEq, Eq)]
96pub enum Cacheable {
97    /// Cacheable
98    Yes,
99    /// Not cacheable
100    No,
101    /// No directive found for explicit cacheability
102    Default,
103}
104
105/// An iter over all the cache control directives
106pub struct ListValueIter<'a>(slice::Split<'a, u8, fn(&u8) -> bool>);
107
108impl<'a> ListValueIter<'a> {
109    pub fn from(value: &'a DirectiveValue) -> Self {
110        ListValueIter(value.parse_as_bytes().split(|byte| byte == &b','))
111    }
112}
113
114// https://datatracker.ietf.org/doc/html/rfc9110#name-whitespace
115// optional whitespace OWS = *(SP / HTAB); SP = 0x20, HTAB = 0x09
116fn trim_ows(bytes: &[u8]) -> &[u8] {
117    fn not_ows(b: &u8) -> bool {
118        b != &b'\x20' && b != &b'\x09'
119    }
120    // find first non-OWS char from front (head) and from end (tail)
121    let head = bytes.iter().position(not_ows).unwrap_or(0);
122    let tail = bytes
123        .iter()
124        .rposition(not_ows)
125        .map(|rpos| rpos + 1)
126        .unwrap_or(head);
127    &bytes[head..tail]
128}
129
130impl<'a> Iterator for ListValueIter<'a> {
131    type Item = &'a [u8];
132
133    fn next(&mut self) -> Option<Self::Item> {
134        Some(trim_ows(self.0.next()?))
135    }
136}
137
138// Originally from https://github.com/hapijs/wreck which has the following comments:
139// Cache-Control   = 1#cache-directive
140// cache-directive = token [ "=" ( token / quoted-string ) ]
141// token           = [^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+
142// quoted-string   = "(?:[^"\\]|\\.)*"
143//
144// note the `token` implementation excludes disallowed ASCII ranges
145// and disallowed delimiters: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
146// though it does not forbid `obs-text`: %x80-FF
147static RE_CACHE_DIRECTIVE: Lazy<Regex> =
148    // to break our version down further:
149    // `(?-u)`: unicode support disabled, which puts the regex into "ASCII compatible mode" for specifying literal bytes like \x7F: https://docs.rs/regex/1.10.4/regex/bytes/index.html#syntax
150    // `(?:^|(?:\s*[,;]\s*)`: allow either , or ; as a delimiter
151    // `([^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+)`: token (directive name capture group)
152    // `(?:=((?:[^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+|(?:"(?:[^"\\]|\\.)*"))))`: token OR quoted-string (directive value capture-group)
153    Lazy::new(|| {
154        Regex::new(r#"(?-u)(?:^|(?:\s*[,;]\s*))([^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+)(?:=((?:[^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+|(?:"(?:[^"\\]|\\.)*"))))?"#).unwrap()
155    });
156
157impl CacheControl {
158    // Our parsing strategy is more permissive than the RFC in a few ways:
159    // - Allows semicolons as delimiters (in addition to commas). See the regex above.
160    // - Allows octets outside of visible ASCII in `token`s, and in later RFCs, octets outside of
161    //   the `quoted-string` range: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
162    //   See the regex above.
163    // - Doesn't require no-value for "boolean directives," such as must-revalidate
164    // - Allows quoted-string format for numeric values.
165    fn from_headers(headers: http::header::GetAll<HeaderValue>) -> Option<Self> {
166        let mut directives = IndexMap::new();
167        // should iterate in header line insertion order
168        for line in headers {
169            for captures in RE_CACHE_DIRECTIVE.captures_iter(line.as_bytes()) {
170                // directive key
171                // header values don't have to be utf-8, but we store keys as strings for case-insensitive hashing
172                let key = captures.get(1).and_then(|cap| {
173                    str::from_utf8(cap.as_bytes())
174                        .ok()
175                        .map(|token| token.to_lowercase())
176                });
177                if key.is_none() {
178                    continue;
179                }
180                // directive value
181                // match token or quoted-string
182                let value = captures
183                    .get(2)
184                    .map(|cap| DirectiveValue(cap.as_bytes().to_vec()));
185                directives.insert(key.unwrap(), value);
186            }
187        }
188        Some(CacheControl { directives })
189    }
190
191    /// Parse from the given header name in `headers`
192    pub fn from_headers_named(header_name: &str, headers: &http::HeaderMap) -> Option<Self> {
193        if !headers.contains_key(header_name) {
194            return None;
195        }
196
197        Self::from_headers(headers.get_all(header_name))
198    }
199
200    /// Parse from the given header name in the [ReqHeader]
201    pub fn from_req_headers_named(header_name: &str, req_header: &ReqHeader) -> Option<Self> {
202        Self::from_headers_named(header_name, &req_header.headers)
203    }
204
205    /// Parse `Cache-Control` header name from the [ReqHeader]
206    pub fn from_req_headers(req_header: &ReqHeader) -> Option<Self> {
207        Self::from_req_headers_named("cache-control", req_header)
208    }
209
210    /// Parse from the given header name in the [RespHeader]
211    pub fn from_resp_headers_named(header_name: &str, resp_header: &RespHeader) -> Option<Self> {
212        Self::from_headers_named(header_name, &resp_header.headers)
213    }
214
215    /// Parse `Cache-Control` header name from the [RespHeader]
216    pub fn from_resp_headers(resp_header: &RespHeader) -> Option<Self> {
217        Self::from_resp_headers_named("cache-control", resp_header)
218    }
219
220    /// Whether the given directive is in the cache control.
221    pub fn has_key(&self, key: &str) -> bool {
222        self.directives.contains_key(key)
223    }
224
225    /// Whether the `public` directive is in the cache control.
226    pub fn public(&self) -> bool {
227        self.has_key("public")
228    }
229
230    /// Whether the given directive exists, and it has no value.
231    fn has_key_without_value(&self, key: &str) -> bool {
232        matches!(self.directives.get(key), Some(None))
233    }
234
235    /// Whether the standalone `private` exists in the cache control
236    // RFC 7234: using the #field-name versions of `private`
237    // means a shared cache "MUST NOT store the specified field-name(s),
238    // whereas it MAY store the remainder of the response."
239    // It must be a boolean form (no value) to apply to the whole response.
240    // https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.6
241    pub fn private(&self) -> bool {
242        self.has_key_without_value("private")
243    }
244
245    fn get_field_names(&self, key: &str) -> Option<ListValueIter> {
246        if let Some(Some(value)) = self.directives.get(key) {
247            Some(ListValueIter::from(value))
248        } else {
249            None
250        }
251    }
252
253    /// Get the values of `private=`
254    pub fn private_field_names(&self) -> Option<ListValueIter> {
255        self.get_field_names("private")
256    }
257
258    /// Whether the standalone `no-cache` exists in the cache control
259    pub fn no_cache(&self) -> bool {
260        self.has_key_without_value("no-cache")
261    }
262
263    /// Get the values of `no-cache=`
264    pub fn no_cache_field_names(&self) -> Option<ListValueIter> {
265        self.get_field_names("no-cache")
266    }
267
268    /// Whether `no-store` exists.
269    pub fn no_store(&self) -> bool {
270        self.has_key("no-store")
271    }
272
273    fn parse_delta_seconds(&self, key: &str) -> Result<Option<u32>> {
274        if let Some(Some(dir_value)) = self.directives.get(key) {
275            Ok(Some(dir_value.parse_as_delta_seconds()?))
276        } else {
277            Ok(None)
278        }
279    }
280
281    /// Return the `max-age` seconds
282    pub fn max_age(&self) -> Result<Option<u32>> {
283        self.parse_delta_seconds("max-age")
284    }
285
286    /// Return the `s-maxage` seconds
287    pub fn s_maxage(&self) -> Result<Option<u32>> {
288        self.parse_delta_seconds("s-maxage")
289    }
290
291    /// Return the `stale-while-revalidate` seconds
292    pub fn stale_while_revalidate(&self) -> Result<Option<u32>> {
293        self.parse_delta_seconds("stale-while-revalidate")
294    }
295
296    /// Return the `stale-if-error` seconds
297    pub fn stale_if_error(&self) -> Result<Option<u32>> {
298        self.parse_delta_seconds("stale-if-error")
299    }
300
301    /// Whether `must-revalidate` exists.
302    pub fn must_revalidate(&self) -> bool {
303        self.has_key("must-revalidate")
304    }
305
306    /// Whether `proxy-revalidate` exists.
307    pub fn proxy_revalidate(&self) -> bool {
308        self.has_key("proxy-revalidate")
309    }
310
311    /// Whether `only-if-cached` exists.
312    pub fn only_if_cached(&self) -> bool {
313        self.has_key("only-if-cached")
314    }
315}
316
317impl InterpretCacheControl for CacheControl {
318    fn is_cacheable(&self) -> Cacheable {
319        if self.no_store() || self.private() {
320            return Cacheable::No;
321        }
322        if self.has_key("s-maxage") || self.has_key("max-age") || self.public() {
323            return Cacheable::Yes;
324        }
325        Cacheable::Default
326    }
327
328    fn allow_caching_authorized_req(&self) -> bool {
329        // RFC 7234 https://datatracker.ietf.org/doc/html/rfc7234#section-3
330        // "MUST NOT" store requests with Authorization header
331        // unless response contains one of these directives
332        self.must_revalidate() || self.public() || self.has_key("s-maxage")
333    }
334
335    fn fresh_sec(&self) -> Option<u32> {
336        if self.no_cache() {
337            // always treated as stale
338            return Some(0);
339        }
340        match self.s_maxage() {
341            Ok(Some(seconds)) => Some(seconds),
342            // s-maxage not present
343            Ok(None) => match self.max_age() {
344                Ok(Some(seconds)) => Some(seconds),
345                _ => None,
346            },
347            _ => None,
348        }
349    }
350
351    fn serve_stale_while_revalidate_sec(&self) -> Option<u32> {
352        // RFC 7234: these directives forbid serving stale.
353        // https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.4
354        if self.must_revalidate() || self.proxy_revalidate() || self.has_key("s-maxage") {
355            return Some(0);
356        }
357        self.stale_while_revalidate().unwrap_or(None)
358    }
359
360    fn serve_stale_if_error_sec(&self) -> Option<u32> {
361        if self.must_revalidate() || self.proxy_revalidate() || self.has_key("s-maxage") {
362            return Some(0);
363        }
364        self.stale_if_error().unwrap_or(None)
365    }
366
367    // Strip header names listed in `private` or `no-cache` directives from a response.
368    fn strip_private_headers(&self, resp_header: &mut ResponseHeader) {
369        fn strip_listed_headers(resp: &mut ResponseHeader, field_names: ListValueIter) {
370            for name in field_names {
371                if let Ok(header) = HeaderName::from_bytes(name) {
372                    resp.remove_header(&header);
373                }
374            }
375        }
376
377        if let Some(headers) = self.private_field_names() {
378            strip_listed_headers(resp_header, headers);
379        }
380        // We interpret `no-cache` the same way as `private`,
381        // though technically it has a less restrictive requirement
382        // ("MUST NOT be sent in the response to a subsequent request
383        // without successful revalidation with the origin server").
384        // https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.2
385        if let Some(headers) = self.no_cache_field_names() {
386            strip_listed_headers(resp_header, headers);
387        }
388    }
389}
390
391/// `InterpretCacheControl` provides a meaningful interface to the parsed `CacheControl`.
392/// These functions actually interpret the parsed cache-control directives to return
393/// the freshness or other cache meta values that cache-control is signaling.
394///
395/// By default `CacheControl` implements an RFC-7234 compliant reading that assumes it is being
396/// used with a shared (proxy) cache.
397pub trait InterpretCacheControl {
398    /// Does cache-control specify this response is cacheable?
399    ///
400    /// Note that an RFC-7234 compliant cacheability check must also
401    /// check if the request contained the Authorization header and
402    /// `allow_caching_authorized_req`.
403    fn is_cacheable(&self) -> Cacheable;
404
405    /// Does this cache-control allow caching a response to
406    /// a request with the Authorization header?
407    fn allow_caching_authorized_req(&self) -> bool;
408
409    /// Returns freshness ttl specified in cache-control
410    ///
411    /// - `Some(_)` indicates cache-control specifies a valid ttl. Some(0) = always stale.
412    /// - `None` means cache-control did not specify a valid ttl.
413    fn fresh_sec(&self) -> Option<u32>;
414
415    /// Returns stale-while-revalidate ttl,
416    ///
417    /// The result should consider all the relevant cache directives, not just SWR header itself.
418    ///
419    /// Some(0) means serving such stale is disallowed by directive like `must-revalidate`
420    /// or `stale-while-revalidater=0`.
421    ///
422    /// `None` indicates no SWR ttl was specified.
423    fn serve_stale_while_revalidate_sec(&self) -> Option<u32>;
424
425    /// Returns stale-if-error ttl,
426    ///
427    /// The result should consider all the relevant cache directives, not just SIE header itself.
428    ///
429    /// Some(0) means serving such stale is disallowed by directive like `must-revalidate`
430    /// or `stale-if-error=0`.
431    ///
432    /// `None` indicates no SIE ttl was specified.
433    fn serve_stale_if_error_sec(&self) -> Option<u32>;
434
435    /// Strip header names listed in `private` or `no-cache` directives from a response,
436    /// usually prior to storing that response in cache.
437    fn strip_private_headers(&self, resp_header: &mut ResponseHeader);
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use http::header::CACHE_CONTROL;
444    use http::{request, response};
445
446    fn build_response(cc_key: HeaderName, cc_value: &str) -> response::Parts {
447        let (parts, _) = response::Builder::new()
448            .header(cc_key, cc_value)
449            .body(())
450            .unwrap()
451            .into_parts();
452        parts
453    }
454
455    #[test]
456    fn test_simple_cache_control() {
457        let resp = build_response(CACHE_CONTROL, "public, max-age=10000");
458        let cc = CacheControl::from_resp_headers(&resp).unwrap();
459        assert!(cc.public());
460        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
461    }
462
463    #[test]
464    fn test_private_cache_control() {
465        let resp = build_response(CACHE_CONTROL, "private");
466        let cc = CacheControl::from_resp_headers(&resp).unwrap();
467
468        assert!(cc.private());
469        assert!(cc.max_age().unwrap().is_none());
470    }
471
472    #[test]
473    fn test_directives_across_header_lines() {
474        let (parts, _) = response::Builder::new()
475            .header(CACHE_CONTROL, "public,")
476            .header("cache-Control", "max-age=10000")
477            .body(())
478            .unwrap()
479            .into_parts();
480        let cc = CacheControl::from_resp_headers(&parts).unwrap();
481
482        assert!(cc.public());
483        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
484    }
485
486    #[test]
487    fn test_recognizes_semicolons_as_delimiters() {
488        let resp = build_response(CACHE_CONTROL, "public; max-age=0");
489        let cc = CacheControl::from_resp_headers(&resp).unwrap();
490
491        assert!(cc.public());
492        assert_eq!(cc.max_age().unwrap().unwrap(), 0);
493    }
494
495    #[test]
496    fn test_unknown_directives() {
497        let resp = build_response(CACHE_CONTROL, "public,random1=random2, rand3=\"\"");
498        let cc = CacheControl::from_resp_headers(&resp).unwrap();
499        let mut directive_iter = cc.directives.iter();
500
501        let first = directive_iter.next().unwrap();
502        assert_eq!(first.0, &"public");
503        assert!(first.1.is_none());
504
505        let second = directive_iter.next().unwrap();
506        assert_eq!(second.0, &"random1");
507        assert_eq!(second.1.as_ref().unwrap().0, "random2".as_bytes());
508
509        let third = directive_iter.next().unwrap();
510        assert_eq!(third.0, &"rand3");
511        assert_eq!(third.1.as_ref().unwrap().0, "\"\"".as_bytes());
512
513        assert!(directive_iter.next().is_none());
514    }
515
516    #[test]
517    fn test_case_insensitive_directive_keys() {
518        let resp = build_response(
519            CACHE_CONTROL,
520            "Public=\"something\", mAx-AGe=\"10000\", foo=cRaZyCaSe, bAr=\"inQuotes\"",
521        );
522        let cc = CacheControl::from_resp_headers(&resp).unwrap();
523
524        assert!(cc.public());
525        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
526
527        let mut directive_iter = cc.directives.iter();
528        let first = directive_iter.next().unwrap();
529        assert_eq!(first.0, &"public");
530        assert_eq!(first.1.as_ref().unwrap().0, "\"something\"".as_bytes());
531
532        let second = directive_iter.next().unwrap();
533        assert_eq!(second.0, &"max-age");
534        assert_eq!(second.1.as_ref().unwrap().0, "\"10000\"".as_bytes());
535
536        // values are still stored with casing
537        let third = directive_iter.next().unwrap();
538        assert_eq!(third.0, &"foo");
539        assert_eq!(third.1.as_ref().unwrap().0, "cRaZyCaSe".as_bytes());
540
541        let fourth = directive_iter.next().unwrap();
542        assert_eq!(fourth.0, &"bar");
543        assert_eq!(fourth.1.as_ref().unwrap().0, "\"inQuotes\"".as_bytes());
544
545        assert!(directive_iter.next().is_none());
546    }
547
548    #[test]
549    fn test_non_ascii() {
550        let resp = build_response(CACHE_CONTROL, "püblic=💖, max-age=\"💯\"");
551        let cc = CacheControl::from_resp_headers(&resp).unwrap();
552
553        // Not considered valid registered directive keys / values
554        assert!(!cc.public());
555        assert_eq!(
556            cc.max_age().unwrap_err().context.unwrap().to_string(),
557            "could not parse value as u32"
558        );
559
560        let mut directive_iter = cc.directives.iter();
561        let first = directive_iter.next().unwrap();
562        assert_eq!(first.0, &"püblic");
563        assert_eq!(first.1.as_ref().unwrap().0, "💖".as_bytes());
564
565        let second = directive_iter.next().unwrap();
566        assert_eq!(second.0, &"max-age");
567        assert_eq!(second.1.as_ref().unwrap().0, "\"💯\"".as_bytes());
568
569        assert!(directive_iter.next().is_none());
570    }
571
572    #[test]
573    fn test_non_utf8_key() {
574        let mut resp = response::Builder::new().body(()).unwrap();
575        resp.headers_mut().insert(
576            CACHE_CONTROL,
577            HeaderValue::from_bytes(b"bar\xFF=\"baz\", a=b").unwrap(),
578        );
579        let (parts, _) = resp.into_parts();
580        let cc = CacheControl::from_resp_headers(&parts).unwrap();
581
582        // invalid bytes for key
583        let mut directive_iter = cc.directives.iter();
584        let first = directive_iter.next().unwrap();
585        assert_eq!(first.0, &"a");
586        assert_eq!(first.1.as_ref().unwrap().0, "b".as_bytes());
587
588        assert!(directive_iter.next().is_none());
589    }
590
591    #[test]
592    fn test_non_utf8_value() {
593        // RFC 7230: 0xFF is part of obs-text and is officially considered a valid octet in quoted-strings
594        let mut resp = response::Builder::new().body(()).unwrap();
595        resp.headers_mut().insert(
596            CACHE_CONTROL,
597            HeaderValue::from_bytes(b"max-age=ba\xFFr, bar=\"baz\xFF\", a=b").unwrap(),
598        );
599        let (parts, _) = resp.into_parts();
600        let cc = CacheControl::from_resp_headers(&parts).unwrap();
601
602        assert_eq!(
603            cc.max_age().unwrap_err().context.unwrap().to_string(),
604            "could not parse value as utf8"
605        );
606
607        let mut directive_iter = cc.directives.iter();
608
609        let first = directive_iter.next().unwrap();
610        assert_eq!(first.0, &"max-age");
611        assert_eq!(first.1.as_ref().unwrap().0, b"ba\xFFr");
612
613        let second = directive_iter.next().unwrap();
614        assert_eq!(second.0, &"bar");
615        assert_eq!(second.1.as_ref().unwrap().0, b"\"baz\xFF\"");
616
617        let third = directive_iter.next().unwrap();
618        assert_eq!(third.0, &"a");
619        assert_eq!(third.1.as_ref().unwrap().0, "b".as_bytes());
620
621        assert!(directive_iter.next().is_none());
622    }
623
624    #[test]
625    fn test_age_overflow() {
626        let resp = build_response(
627            CACHE_CONTROL,
628            "max-age=-99999999999999999999999999, s-maxage=99999999999999999999999999",
629        );
630        let cc = CacheControl::from_resp_headers(&resp).unwrap();
631
632        assert_eq!(
633            cc.s_maxage().unwrap().unwrap(),
634            DELTA_SECONDS_OVERFLOW_VALUE
635        );
636        // negative ages still result in errors even with overflow handling
637        assert_eq!(
638            cc.max_age().unwrap_err().context.unwrap().to_string(),
639            "could not parse value as u32"
640        );
641    }
642
643    #[test]
644    fn test_fresh_sec() {
645        let resp = build_response(CACHE_CONTROL, "");
646        let cc = CacheControl::from_resp_headers(&resp).unwrap();
647        assert!(cc.fresh_sec().is_none());
648
649        let resp = build_response(CACHE_CONTROL, "max-age=12345");
650        let cc = CacheControl::from_resp_headers(&resp).unwrap();
651        assert_eq!(cc.fresh_sec().unwrap(), 12345);
652
653        let resp = build_response(CACHE_CONTROL, "max-age=99999,s-maxage=123");
654        let cc = CacheControl::from_resp_headers(&resp).unwrap();
655        // prefer s-maxage over max-age
656        assert_eq!(cc.fresh_sec().unwrap(), 123);
657    }
658
659    #[test]
660    fn test_cacheability() {
661        let resp = build_response(CACHE_CONTROL, "");
662        let cc = CacheControl::from_resp_headers(&resp).unwrap();
663        assert_eq!(cc.is_cacheable(), Cacheable::Default);
664
665        // uncacheable
666        let resp = build_response(CACHE_CONTROL, "private, max-age=12345");
667        let cc = CacheControl::from_resp_headers(&resp).unwrap();
668        assert_eq!(cc.is_cacheable(), Cacheable::No);
669
670        let resp = build_response(CACHE_CONTROL, "no-store, max-age=12345");
671        let cc = CacheControl::from_resp_headers(&resp).unwrap();
672        assert_eq!(cc.is_cacheable(), Cacheable::No);
673
674        // cacheable
675        let resp = build_response(CACHE_CONTROL, "public");
676        let cc = CacheControl::from_resp_headers(&resp).unwrap();
677        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
678
679        let resp = build_response(CACHE_CONTROL, "max-age=0");
680        let cc = CacheControl::from_resp_headers(&resp).unwrap();
681        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
682    }
683
684    #[test]
685    fn test_no_cache() {
686        let resp = build_response(CACHE_CONTROL, "no-cache, max-age=12345");
687        let cc = CacheControl::from_resp_headers(&resp).unwrap();
688        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
689        assert_eq!(cc.fresh_sec().unwrap(), 0);
690    }
691
692    #[test]
693    fn test_no_cache_field_names() {
694        let resp = build_response(CACHE_CONTROL, "no-cache=\"set-cookie\", max-age=12345");
695        let cc = CacheControl::from_resp_headers(&resp).unwrap();
696        assert!(!cc.private());
697        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
698        assert_eq!(cc.fresh_sec().unwrap(), 12345);
699        let mut field_names = cc.no_cache_field_names().unwrap();
700        assert_eq!(
701            str::from_utf8(field_names.next().unwrap()).unwrap(),
702            "set-cookie"
703        );
704        assert!(field_names.next().is_none());
705
706        let mut resp = response::Builder::new().body(()).unwrap();
707        resp.headers_mut().insert(
708            CACHE_CONTROL,
709            HeaderValue::from_bytes(
710                b"private=\"\", no-cache=\"a\xFF, set-cookie, Baz\x09 , c,d  ,, \"",
711            )
712            .unwrap(),
713        );
714        let (parts, _) = resp.into_parts();
715        let cc = CacheControl::from_resp_headers(&parts).unwrap();
716        let mut field_names = cc.private_field_names().unwrap();
717        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
718        assert!(field_names.next().is_none());
719        let mut field_names = cc.no_cache_field_names().unwrap();
720        assert!(str::from_utf8(field_names.next().unwrap()).is_err());
721        assert_eq!(
722            str::from_utf8(field_names.next().unwrap()).unwrap(),
723            "set-cookie"
724        );
725        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "Baz");
726        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "c");
727        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "d");
728        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
729        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
730        assert!(field_names.next().is_none());
731    }
732
733    #[test]
734    fn test_strip_private_headers() {
735        let mut resp = ResponseHeader::build(200, None).unwrap();
736        resp.append_header(
737            CACHE_CONTROL,
738            "no-cache=\"x-private-header\", max-age=12345",
739        )
740        .unwrap();
741        resp.append_header("X-Private-Header", "dropped").unwrap();
742
743        let cc = CacheControl::from_resp_headers(&resp).unwrap();
744        cc.strip_private_headers(&mut resp);
745        assert!(!resp.headers.contains_key("X-Private-Header"));
746    }
747
748    #[test]
749    fn test_stale_while_revalidate() {
750        let resp = build_response(CACHE_CONTROL, "max-age=12345, stale-while-revalidate=5");
751        let cc = CacheControl::from_resp_headers(&resp).unwrap();
752        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 5);
753        assert_eq!(cc.serve_stale_while_revalidate_sec().unwrap(), 5);
754        assert!(cc.serve_stale_if_error_sec().is_none());
755    }
756
757    #[test]
758    fn test_stale_if_error() {
759        let resp = build_response(CACHE_CONTROL, "max-age=12345, stale-if-error=3600");
760        let cc = CacheControl::from_resp_headers(&resp).unwrap();
761        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 3600);
762        assert_eq!(cc.serve_stale_if_error_sec().unwrap(), 3600);
763        assert!(cc.serve_stale_while_revalidate_sec().is_none());
764    }
765
766    #[test]
767    fn test_must_revalidate() {
768        let resp = build_response(
769            CACHE_CONTROL,
770            "max-age=12345, stale-while-revalidate=60, stale-if-error=30, must-revalidate",
771        );
772        let cc = CacheControl::from_resp_headers(&resp).unwrap();
773        assert!(cc.must_revalidate());
774        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
775        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
776        assert_eq!(cc.serve_stale_while_revalidate_sec().unwrap(), 0);
777        assert_eq!(cc.serve_stale_if_error_sec().unwrap(), 0);
778    }
779
780    #[test]
781    fn test_proxy_revalidate() {
782        let resp = build_response(
783            CACHE_CONTROL,
784            "max-age=12345, stale-while-revalidate=60, stale-if-error=30, proxy-revalidate",
785        );
786        let cc = CacheControl::from_resp_headers(&resp).unwrap();
787        assert!(cc.proxy_revalidate());
788        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
789        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
790        assert_eq!(cc.serve_stale_while_revalidate_sec().unwrap(), 0);
791        assert_eq!(cc.serve_stale_if_error_sec().unwrap(), 0);
792    }
793
794    #[test]
795    fn test_s_maxage_stale() {
796        let resp = build_response(
797            CACHE_CONTROL,
798            "s-maxage=0, stale-while-revalidate=60, stale-if-error=30",
799        );
800        let cc = CacheControl::from_resp_headers(&resp).unwrap();
801        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
802        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
803        assert_eq!(cc.serve_stale_while_revalidate_sec().unwrap(), 0);
804        assert_eq!(cc.serve_stale_if_error_sec().unwrap(), 0);
805    }
806
807    #[test]
808    fn test_authorized_request() {
809        let resp = build_response(CACHE_CONTROL, "max-age=10");
810        let cc = CacheControl::from_resp_headers(&resp).unwrap();
811        assert!(!cc.allow_caching_authorized_req());
812
813        let resp = build_response(CACHE_CONTROL, "s-maxage=10");
814        let cc = CacheControl::from_resp_headers(&resp).unwrap();
815        assert!(cc.allow_caching_authorized_req());
816
817        let resp = build_response(CACHE_CONTROL, "public");
818        let cc = CacheControl::from_resp_headers(&resp).unwrap();
819        assert!(cc.allow_caching_authorized_req());
820
821        let resp = build_response(CACHE_CONTROL, "must-revalidate, max-age=0");
822        let cc = CacheControl::from_resp_headers(&resp).unwrap();
823        assert!(cc.allow_caching_authorized_req());
824
825        let resp = build_response(CACHE_CONTROL, "");
826        let cc = CacheControl::from_resp_headers(&resp).unwrap();
827        assert!(!cc.allow_caching_authorized_req());
828    }
829
830    fn build_request(cc_key: HeaderName, cc_value: &str) -> request::Parts {
831        let (parts, _) = request::Builder::new()
832            .header(cc_key, cc_value)
833            .body(())
834            .unwrap()
835            .into_parts();
836        parts
837    }
838
839    #[test]
840    fn test_request_only_if_cached() {
841        let req = build_request(CACHE_CONTROL, "only-if-cached=1");
842        let cc = CacheControl::from_req_headers(&req).unwrap();
843        assert!(cc.only_if_cached())
844    }
845}