Skip to main content

pingora_cache/
cache_control.rs

1// Copyright 2026 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::fmt;
26use std::num::IntErrorKind;
27use std::slice;
28use std::str;
29
30/// The max delta-second per [RFC 9111](https://datatracker.ietf.org/doc/html/rfc9111#section-1.2.2)
31// "If a cache receives a delta-seconds value
32// greater than the greatest integer it can represent, or if any of its
33// subsequent calculations overflows, the cache MUST consider the value
34// to be 2147483648 (2^31) or the greatest positive integer it can
35// conveniently represent.
36//
37//    |  *Note:* The value 2147483648 is here for historical reasons,
38//    |  represents infinity (over 68 years), and does not need to be
39//    |  stored in binary form; an implementation could produce it as a
40//    |  string if any overflow occurs, even if the calculations are
41//    |  performed with an arithmetic type incapable of directly
42//    |  representing that number.  What matters here is that an
43//    |  overflow be detected and not treated as a negative value in
44//    |  later calculations."
45//
46// We choose to use i32::MAX for our overflow value to stick to the letter of the RFC.
47pub const DELTA_SECONDS_OVERFLOW_VALUE: u32 = i32::MAX as u32;
48pub const DELTA_SECONDS_OVERFLOW_DURATION: Duration =
49    Duration::from_secs(DELTA_SECONDS_OVERFLOW_VALUE as u64);
50
51/// Cache-Control directive key.
52///
53/// Known RFC 9111 (and common extension) directives are represented as enum variants,
54/// avoiding a heap allocation for every parsed directive. Unknown or extension
55/// directives are preserved via the [`Unknown`](Self::Unknown) variant.
56///
57/// Directive keys are always stored in lowercase, matching the case-insensitive
58/// comparison required by the HTTP specification.
59#[derive(Clone, Debug, PartialEq, Eq, Hash)]
60pub enum DirectiveKey {
61    /// `max-age`
62    MaxAge,
63    /// `s-maxage`
64    SMaxAge,
65    /// `no-cache`
66    NoCache,
67    /// `no-store`
68    NoStore,
69    /// `private`
70    Private,
71    /// `public`
72    Public,
73    /// `must-revalidate`
74    MustRevalidate,
75    /// `proxy-revalidate`
76    ProxyRevalidate,
77    /// `must-understand`
78    MustUnderstand,
79    /// `no-transform`
80    NoTransform,
81    /// `immutable`
82    Immutable,
83    /// `stale-while-revalidate`
84    StaleWhileRevalidate,
85    /// `stale-if-error`
86    StaleIfError,
87    /// `only-if-cached`
88    OnlyIfCached,
89    /// An unknown or extension directive, stored as the original (lowercased) name.
90    Unknown(String),
91}
92
93impl DirectiveKey {
94    /// Return the wire-format name of this directive as a `&str`.
95    ///
96    /// Known variants return a `&'static str`; [`Unknown`](Self::Unknown)
97    /// returns a borrow of the inner `String`.
98    pub fn as_str(&self) -> &str {
99        match self {
100            Self::MaxAge => "max-age",
101            Self::SMaxAge => "s-maxage",
102            Self::NoCache => "no-cache",
103            Self::NoStore => "no-store",
104            Self::Private => "private",
105            Self::Public => "public",
106            Self::MustRevalidate => "must-revalidate",
107            Self::ProxyRevalidate => "proxy-revalidate",
108            Self::MustUnderstand => "must-understand",
109            Self::NoTransform => "no-transform",
110            Self::Immutable => "immutable",
111            Self::StaleWhileRevalidate => "stale-while-revalidate",
112            Self::StaleIfError => "stale-if-error",
113            Self::OnlyIfCached => "only-if-cached",
114            Self::Unknown(s) => s.as_str(),
115        }
116    }
117
118    /// Construct a [`DirectiveKey`] from a **lowercase** `&str`.
119    ///
120    /// Known directive names are mapped to their enum variant (zero allocation).
121    /// Anything else produces [`Unknown`](Self::Unknown) with an owned copy of
122    /// the string.
123    ///
124    /// The caller is responsible for lowercasing the input first; this function
125    /// does **not** perform case folding.
126    ///
127    /// See also [`from_lowercase_owned`](Self::from_lowercase_owned) to avoid a
128    /// re-allocation when the caller already has an owned `String`.
129    pub fn from_lowercase(s: &str) -> Self {
130        match s {
131            "max-age" => Self::MaxAge,
132            "s-maxage" => Self::SMaxAge,
133            "no-cache" => Self::NoCache,
134            "no-store" => Self::NoStore,
135            "private" => Self::Private,
136            "public" => Self::Public,
137            "must-revalidate" => Self::MustRevalidate,
138            "proxy-revalidate" => Self::ProxyRevalidate,
139            "must-understand" => Self::MustUnderstand,
140            "no-transform" => Self::NoTransform,
141            "immutable" => Self::Immutable,
142            "stale-while-revalidate" => Self::StaleWhileRevalidate,
143            "stale-if-error" => Self::StaleIfError,
144            "only-if-cached" => Self::OnlyIfCached,
145            other => Self::Unknown(other.to_owned()),
146        }
147    }
148
149    /// Construct a [`DirectiveKey`] from a **lowercase** owned `String`.
150    ///
151    /// Behaves identically to [`from_lowercase`](Self::from_lowercase) but
152    /// takes ownership of the `String`, avoiding a re-allocation when the
153    /// input maps to [`Unknown`](Self::Unknown).
154    pub fn from_lowercase_owned(s: String) -> Self {
155        // Check against known names first (the string is borrowed for matching).
156        match s.as_str() {
157            "max-age" => Self::MaxAge,
158            "s-maxage" => Self::SMaxAge,
159            "no-cache" => Self::NoCache,
160            "no-store" => Self::NoStore,
161            "private" => Self::Private,
162            "public" => Self::Public,
163            "must-revalidate" => Self::MustRevalidate,
164            "proxy-revalidate" => Self::ProxyRevalidate,
165            "must-understand" => Self::MustUnderstand,
166            "no-transform" => Self::NoTransform,
167            "immutable" => Self::Immutable,
168            "stale-while-revalidate" => Self::StaleWhileRevalidate,
169            "stale-if-error" => Self::StaleIfError,
170            "only-if-cached" => Self::OnlyIfCached,
171            _ => Self::Unknown(s),
172        }
173    }
174}
175
176impl fmt::Display for DirectiveKey {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.write_str(self.as_str())
179    }
180}
181
182impl PartialEq<str> for DirectiveKey {
183    fn eq(&self, other: &str) -> bool {
184        self.as_str() == other
185    }
186}
187
188impl PartialEq<&str> for DirectiveKey {
189    fn eq(&self, other: &&str) -> bool {
190        self.as_str() == *other
191    }
192}
193
194/// Cache control directive value type
195#[derive(Debug)]
196pub struct DirectiveValue(pub Vec<u8>);
197
198impl AsRef<[u8]> for DirectiveValue {
199    fn as_ref(&self) -> &[u8] {
200        &self.0
201    }
202}
203
204impl DirectiveValue {
205    /// A [DirectiveValue] without quotes (`"`).
206    pub fn parse_as_bytes(&self) -> &[u8] {
207        self.0
208            .strip_prefix(b"\"")
209            .and_then(|bytes| bytes.strip_suffix(b"\""))
210            .unwrap_or(&self.0[..])
211    }
212
213    /// A [DirectiveValue] without quotes (`"`) as `str`.
214    pub fn parse_as_str(&self) -> Result<&str> {
215        str::from_utf8(self.parse_as_bytes()).or_else(|e| {
216            Error::e_because(ErrorType::InternalError, "could not parse value as utf8", e)
217        })
218    }
219
220    /// Parse the [DirectiveValue] as delta seconds
221    ///
222    /// `"`s are ignored. The value is capped to [DELTA_SECONDS_OVERFLOW_VALUE].
223    pub fn parse_as_delta_seconds(&self) -> Result<u32> {
224        match self.parse_as_str()?.parse::<u32>() {
225            Ok(value) => Ok(value),
226            Err(e) => {
227                // delta-seconds expect to handle positive overflow gracefully
228                if e.kind() == &IntErrorKind::PosOverflow {
229                    Ok(DELTA_SECONDS_OVERFLOW_VALUE)
230                } else {
231                    Error::e_because(ErrorType::InternalError, "could not parse value as u32", e)
232                }
233            }
234        }
235    }
236
237    /// Parse the [DirectiveValue] as delta seconds, permitting a fractional component.
238    ///
239    /// Values with a fractional part are rounded down (floored) to the nearest
240    /// non-negative integer: e.g. `1.9` -> `1`, `0.5` -> `0`. This is useful
241    /// for compatibility with upstreams that emit fractional ttls (which
242    /// RFC 9111 strict integer parsing rejects).
243    ///
244    /// Integer parsing is attempted first, so strictly-numeric values (including
245    /// overflow-capped values and quoted integers) behave identically to
246    /// [Self::parse_as_delta_seconds]. Negative, non-finite, or non-numeric
247    /// values still return an error.
248    ///
249    /// `"`s are ignored. The value is capped to [DELTA_SECONDS_OVERFLOW_VALUE].
250    pub fn parse_as_delta_seconds_floor(&self) -> Result<u32> {
251        // UTF-8 validate once; on non-UTF8 input, propagate the same error as
252        // [Self::parse_as_delta_seconds].
253        let s = self.parse_as_str()?;
254        match s.parse::<u32>() {
255            Ok(value) => Ok(value),
256            Err(e) if e.kind() == &IntErrorKind::PosOverflow => Ok(DELTA_SECONDS_OVERFLOW_VALUE),
257            Err(int_err) => {
258                // Fall back to parsing as a non-negative finite float and floor.
259                // On any failure, return an error equivalent to the strict
260                // [Self::parse_as_delta_seconds] u32-parse error.
261                match s.parse::<f64>() {
262                    Ok(f) if f.is_finite() && f >= 0.0 => {
263                        if f >= DELTA_SECONDS_OVERFLOW_VALUE as f64 {
264                            Ok(DELTA_SECONDS_OVERFLOW_VALUE)
265                        } else {
266                            // Safe cast: `f` is finite, non-negative, and strictly
267                            // less than `DELTA_SECONDS_OVERFLOW_VALUE` (i32::MAX),
268                            // which fits in `u32` after flooring.
269                            Ok(f.floor() as u32)
270                        }
271                    }
272                    _ => Error::e_because(
273                        ErrorType::InternalError,
274                        "could not parse value as u32",
275                        int_err,
276                    ),
277                }
278            }
279        }
280    }
281}
282
283/// An ordered map to store cache control key value pairs.
284pub type DirectiveMap = IndexMap<DirectiveKey, Option<DirectiveValue>>;
285
286/// Parsed Cache-Control directives
287#[derive(Debug)]
288pub struct CacheControl {
289    /// The parsed directives
290    pub directives: DirectiveMap,
291    /// When set, delta-seconds directives (`max-age`, `s-maxage`,
292    /// `stale-while-revalidate`, `stale-if-error`) accept fractional values
293    /// and round them down to the nearest non-negative integer.
294    ///
295    /// Defaults to `false`, matching RFC 9111 strict integer parsing. Enable
296    /// via [CacheControl::with_float_seconds] (or by assigning the field
297    /// directly) for contexts that need to interoperate with upstreams that
298    /// emit fractional ttls.
299    pub allow_float_seconds: bool,
300}
301
302/// Cacheability calculated from cache control.
303#[derive(Debug, PartialEq, Eq)]
304pub enum Cacheable {
305    /// Cacheable
306    Yes,
307    /// Not cacheable
308    No,
309    /// No directive found for explicit cacheability
310    Default,
311}
312
313/// An iter over all the cache control directives
314pub struct ListValueIter<'a>(slice::Split<'a, u8, fn(&u8) -> bool>);
315
316impl<'a> ListValueIter<'a> {
317    pub fn from(value: &'a DirectiveValue) -> Self {
318        ListValueIter(value.parse_as_bytes().split(|byte| byte == &b','))
319    }
320}
321
322// https://datatracker.ietf.org/doc/html/rfc9110#name-whitespace
323// optional whitespace OWS = *(SP / HTAB); SP = 0x20, HTAB = 0x09
324fn trim_ows(bytes: &[u8]) -> &[u8] {
325    fn not_ows(b: &u8) -> bool {
326        b != &b'\x20' && b != &b'\x09'
327    }
328    // find first non-OWS char from front (head) and from end (tail)
329    let head = bytes.iter().position(not_ows).unwrap_or(0);
330    let tail = bytes
331        .iter()
332        .rposition(not_ows)
333        .map(|rpos| rpos + 1)
334        .unwrap_or(head);
335    &bytes[head..tail]
336}
337
338impl<'a> Iterator for ListValueIter<'a> {
339    type Item = &'a [u8];
340
341    fn next(&mut self) -> Option<Self::Item> {
342        Some(trim_ows(self.0.next()?))
343    }
344}
345
346// Originally from https://github.com/hapijs/wreck which has the following comments:
347// Cache-Control   = 1#cache-directive
348// cache-directive = token [ "=" ( token / quoted-string ) ]
349// token           = [^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+
350// quoted-string   = "(?:[^"\\]|\\.)*"
351//
352// note the `token` implementation excludes disallowed ASCII ranges
353// and disallowed delimiters: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
354// though it does not forbid `obs-text`: %x80-FF
355static RE_CACHE_DIRECTIVE: Lazy<Regex> =
356    // to break our version down further:
357    // `(?-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
358    // `(?:^|(?:\s*[,;]\s*)`: allow either , or ; as a delimiter
359    // `([^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+)`: token (directive name capture group)
360    // `(?:=((?:[^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+|(?:"(?:[^"\\]|\\.)*"))))`: token OR quoted-string (directive value capture-group)
361    Lazy::new(|| {
362        Regex::new(r#"(?-u)(?:^|(?:\s*[,;]\s*))([^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+)(?:=((?:[^\x00-\x20\(\)<>@,;:\\"/\[\]\?=\{\}\x7F]+|(?:"(?:[^"\\]|\\.)*"))))?"#).unwrap()
363    });
364
365impl CacheControl {
366    // Our parsing strategy is more permissive than the RFC in a few ways:
367    // - Allows semicolons as delimiters (in addition to commas). See the regex above.
368    // - Allows octets outside of visible ASCII in `token`s, and in later RFCs, octets outside of
369    //   the `quoted-string` range: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
370    //   See the regex above.
371    // - Doesn't require no-value for "boolean directives," such as must-revalidate
372    // - Allows quoted-string format for numeric values.
373    fn from_headers(headers: http::header::GetAll<HeaderValue>) -> Option<Self> {
374        let mut directives = IndexMap::new();
375        // should iterate in header line insertion order
376        for line in headers {
377            for captures in RE_CACHE_DIRECTIVE.captures_iter(line.as_bytes()) {
378                // directive key
379                // header values don't have to be utf-8, but we store keys as
380                // strings for case-insensitive matching. Known directive names
381                // are mapped to enum variants (zero allocation).
382                let key = captures.get(1).and_then(|cap| {
383                    str::from_utf8(cap.as_bytes())
384                        .ok()
385                        .map(|token| DirectiveKey::from_lowercase_owned(token.to_lowercase()))
386                });
387                if key.is_none() {
388                    continue;
389                }
390                // directive value
391                // match token or quoted-string
392                let value = captures
393                    .get(2)
394                    .map(|cap| DirectiveValue(cap.as_bytes().to_vec()));
395                directives.insert(key.unwrap(), value);
396            }
397        }
398        Some(CacheControl {
399            directives,
400            allow_float_seconds: false,
401        })
402    }
403
404    /// Builder setter: enable fractional delta-seconds parsing.
405    ///
406    /// See [CacheControl::allow_float_seconds] for semantics. Returns `self`
407    /// so it can be chained onto a parser call, e.g.
408    /// `CacheControl::from_resp_headers(&resp).map(|cc| cc.with_float_seconds())`.
409    pub fn with_float_seconds(mut self) -> Self {
410        self.allow_float_seconds = true;
411        self
412    }
413
414    /// Parse from the given header name in `headers`
415    pub fn from_headers_named(header_name: &str, headers: &http::HeaderMap) -> Option<Self> {
416        if !headers.contains_key(header_name) {
417            return None;
418        }
419
420        Self::from_headers(headers.get_all(header_name))
421    }
422
423    /// Parse from the given header name in the [ReqHeader]
424    pub fn from_req_headers_named(header_name: &str, req_header: &ReqHeader) -> Option<Self> {
425        Self::from_headers_named(header_name, &req_header.headers)
426    }
427
428    /// Parse `Cache-Control` header name from the [ReqHeader]
429    pub fn from_req_headers(req_header: &ReqHeader) -> Option<Self> {
430        Self::from_req_headers_named("cache-control", req_header)
431    }
432
433    /// Parse from the given header name in the [RespHeader]
434    pub fn from_resp_headers_named(header_name: &str, resp_header: &RespHeader) -> Option<Self> {
435        Self::from_headers_named(header_name, &resp_header.headers)
436    }
437
438    /// Parse `Cache-Control` header name from the [RespHeader]
439    pub fn from_resp_headers(resp_header: &RespHeader) -> Option<Self> {
440        Self::from_resp_headers_named("cache-control", resp_header)
441    }
442
443    /// Whether the given directive is in the cache control.
444    ///
445    /// Accepts a string for convenience; known directive names are matched
446    /// without allocation. Prefer [`has_directive`](Self::has_directive) when
447    /// you already have a [`DirectiveKey`].
448    ///
449    /// **Note:** `key` must already be lowercase (e.g. `"max-age"`, not
450    /// `"Max-Age"`). Directive names are stored in wire-format lowercase;
451    /// a mixed-case input will silently miss.
452    pub fn has_key(&self, key: &str) -> bool {
453        self.has_directive(&DirectiveKey::from_lowercase(key))
454    }
455
456    /// Whether the given [`DirectiveKey`] is in the cache control.
457    pub fn has_directive(&self, key: &DirectiveKey) -> bool {
458        self.directives.contains_key(key)
459    }
460
461    /// Whether the `public` directive is in the cache control.
462    pub fn public(&self) -> bool {
463        self.has_directive(&DirectiveKey::Public)
464    }
465
466    /// Whether the given directive exists, and it has no value.
467    fn has_key_without_value(&self, key: &DirectiveKey) -> bool {
468        matches!(self.directives.get(key), Some(None))
469    }
470
471    /// Whether the standalone `private` exists in the cache control
472    // RFC 7234: using the #field-name versions of `private`
473    // means a shared cache "MUST NOT store the specified field-name(s),
474    // whereas it MAY store the remainder of the response."
475    // It must be a boolean form (no value) to apply to the whole response.
476    // https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.6
477    pub fn private(&self) -> bool {
478        self.has_key_without_value(&DirectiveKey::Private)
479    }
480
481    fn get_field_names(&self, key: &DirectiveKey) -> Option<ListValueIter<'_>> {
482        let value = self.directives.get(key)?.as_ref()?;
483        Some(ListValueIter::from(value))
484    }
485
486    /// Get the values of `private=`
487    pub fn private_field_names(&self) -> Option<ListValueIter<'_>> {
488        self.get_field_names(&DirectiveKey::Private)
489    }
490
491    /// Whether the standalone `no-cache` exists in the cache control
492    pub fn no_cache(&self) -> bool {
493        self.has_key_without_value(&DirectiveKey::NoCache)
494    }
495
496    /// Get the values of `no-cache=`
497    pub fn no_cache_field_names(&self) -> Option<ListValueIter<'_>> {
498        self.get_field_names(&DirectiveKey::NoCache)
499    }
500
501    /// Whether `no-store` exists.
502    pub fn no_store(&self) -> bool {
503        self.has_directive(&DirectiveKey::NoStore)
504    }
505
506    fn parse_delta_seconds(&self, key: &DirectiveKey) -> Result<Option<u32>> {
507        if let Some(Some(dir_value)) = self.directives.get(key) {
508            let value = if self.allow_float_seconds {
509                dir_value.parse_as_delta_seconds_floor()?
510            } else {
511                dir_value.parse_as_delta_seconds()?
512            };
513            Ok(Some(value))
514        } else {
515            Ok(None)
516        }
517    }
518
519    /// Return the `max-age` seconds
520    pub fn max_age(&self) -> Result<Option<u32>> {
521        self.parse_delta_seconds(&DirectiveKey::MaxAge)
522    }
523
524    /// Return the `s-maxage` seconds
525    pub fn s_maxage(&self) -> Result<Option<u32>> {
526        self.parse_delta_seconds(&DirectiveKey::SMaxAge)
527    }
528
529    /// Return the `stale-while-revalidate` seconds
530    pub fn stale_while_revalidate(&self) -> Result<Option<u32>> {
531        self.parse_delta_seconds(&DirectiveKey::StaleWhileRevalidate)
532    }
533
534    /// Return the `stale-if-error` seconds
535    pub fn stale_if_error(&self) -> Result<Option<u32>> {
536        self.parse_delta_seconds(&DirectiveKey::StaleIfError)
537    }
538
539    /// Whether `must-revalidate` exists.
540    pub fn must_revalidate(&self) -> bool {
541        self.has_directive(&DirectiveKey::MustRevalidate)
542    }
543
544    /// Whether `proxy-revalidate` exists.
545    pub fn proxy_revalidate(&self) -> bool {
546        self.has_directive(&DirectiveKey::ProxyRevalidate)
547    }
548
549    /// Whether `only-if-cached` exists.
550    pub fn only_if_cached(&self) -> bool {
551        self.has_directive(&DirectiveKey::OnlyIfCached)
552    }
553}
554
555impl InterpretCacheControl for CacheControl {
556    fn is_cacheable(&self) -> Cacheable {
557        if self.no_store() || self.private() {
558            return Cacheable::No;
559        }
560        if self.has_directive(&DirectiveKey::SMaxAge)
561            || self.has_directive(&DirectiveKey::MaxAge)
562            || self.public()
563        {
564            return Cacheable::Yes;
565        }
566        Cacheable::Default
567    }
568
569    fn allow_caching_authorized_req(&self) -> bool {
570        // RFC 7234 https://datatracker.ietf.org/doc/html/rfc7234#section-3
571        // "MUST NOT" store requests with Authorization header
572        // unless response contains one of these directives
573        self.must_revalidate() || self.public() || self.has_directive(&DirectiveKey::SMaxAge)
574    }
575
576    fn fresh_duration(&self) -> Option<Duration> {
577        if self.no_cache() {
578            // always treated as stale
579            return Some(Duration::ZERO);
580        }
581        let seconds = self
582            .s_maxage()
583            .ok()?
584            // s-maxage not present
585            .or_else(|| self.max_age().unwrap_or(None))
586            .map(|duration| Duration::from_secs(duration as u64))?;
587        Some(seconds)
588    }
589
590    fn serve_stale_while_revalidate_duration(&self) -> Option<Duration> {
591        // RFC 7234: these directives forbid serving stale.
592        // https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.4
593        if self.must_revalidate()
594            || self.proxy_revalidate()
595            || self.has_directive(&DirectiveKey::SMaxAge)
596        {
597            return Some(Duration::ZERO);
598        }
599        self.stale_while_revalidate()
600            .unwrap_or(None)
601            .map(|secs| Duration::from_secs(secs as u64))
602    }
603
604    fn serve_stale_if_error_duration(&self) -> Option<Duration> {
605        if self.must_revalidate()
606            || self.proxy_revalidate()
607            || self.has_directive(&DirectiveKey::SMaxAge)
608        {
609            return Some(Duration::ZERO);
610        }
611        self.stale_if_error()
612            .unwrap_or(None)
613            .map(|secs| Duration::from_secs(secs as u64))
614    }
615
616    // Strip header names listed in `private` or `no-cache` directives from a response.
617    fn strip_private_headers(&self, resp_header: &mut ResponseHeader) {
618        fn strip_listed_headers(resp: &mut ResponseHeader, field_names: ListValueIter) {
619            for name in field_names {
620                if let Ok(header) = HeaderName::from_bytes(name) {
621                    resp.remove_header(&header);
622                }
623            }
624        }
625
626        if let Some(headers) = self.private_field_names() {
627            strip_listed_headers(resp_header, headers);
628        }
629        // We interpret `no-cache` the same way as `private`,
630        // though technically it has a less restrictive requirement
631        // ("MUST NOT be sent in the response to a subsequent request
632        // without successful revalidation with the origin server").
633        // https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.2
634        if let Some(headers) = self.no_cache_field_names() {
635            strip_listed_headers(resp_header, headers);
636        }
637    }
638}
639
640/// `InterpretCacheControl` provides a meaningful interface to the parsed `CacheControl`.
641/// These functions actually interpret the parsed cache-control directives to return
642/// the freshness or other cache meta values that cache-control is signaling.
643///
644/// By default `CacheControl` implements an RFC-7234 compliant reading that assumes it is being
645/// used with a shared (proxy) cache.
646pub trait InterpretCacheControl {
647    /// Does cache-control specify this response is cacheable?
648    ///
649    /// Note that an RFC-7234 compliant cacheability check must also
650    /// check if the request contained the Authorization header and
651    /// `allow_caching_authorized_req`.
652    fn is_cacheable(&self) -> Cacheable;
653
654    /// Does this cache-control allow caching a response to
655    /// a request with the Authorization header?
656    fn allow_caching_authorized_req(&self) -> bool;
657
658    /// Returns freshness ttl specified in cache-control
659    ///
660    /// - `Some(_)` indicates cache-control specifies a valid ttl. Some(Duration::ZERO) = always stale.
661    /// - `None` means cache-control did not specify a valid ttl.
662    fn fresh_duration(&self) -> Option<Duration>;
663
664    /// Returns stale-while-revalidate ttl,
665    ///
666    /// The result should consider all the relevant cache directives, not just SWR header itself.
667    ///
668    /// Some(0) means serving such stale is disallowed by directive like `must-revalidate`
669    /// or `stale-while-revalidater=0`.
670    ///
671    /// `None` indicates no SWR ttl was specified.
672    fn serve_stale_while_revalidate_duration(&self) -> Option<Duration>;
673
674    /// Returns stale-if-error ttl,
675    ///
676    /// The result should consider all the relevant cache directives, not just SIE header itself.
677    ///
678    /// Some(0) means serving such stale is disallowed by directive like `must-revalidate`
679    /// or `stale-if-error=0`.
680    ///
681    /// `None` indicates no SIE ttl was specified.
682    fn serve_stale_if_error_duration(&self) -> Option<Duration>;
683
684    /// Strip header names listed in `private` or `no-cache` directives from a response,
685    /// usually prior to storing that response in cache.
686    fn strip_private_headers(&self, resp_header: &mut ResponseHeader);
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use http::header::CACHE_CONTROL;
693    use http::{request, response};
694
695    fn build_response(cc_key: HeaderName, cc_value: &str) -> response::Parts {
696        let (parts, _) = response::Builder::new()
697            .header(cc_key, cc_value)
698            .body(())
699            .unwrap()
700            .into_parts();
701        parts
702    }
703
704    #[test]
705    fn test_simple_cache_control() {
706        let resp = build_response(CACHE_CONTROL, "public, max-age=10000");
707        let cc = CacheControl::from_resp_headers(&resp).unwrap();
708        assert!(cc.public());
709        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
710    }
711
712    #[test]
713    fn test_private_cache_control() {
714        let resp = build_response(CACHE_CONTROL, "private");
715        let cc = CacheControl::from_resp_headers(&resp).unwrap();
716
717        assert!(cc.private());
718        assert!(cc.max_age().unwrap().is_none());
719    }
720
721    #[test]
722    fn test_directives_across_header_lines() {
723        let (parts, _) = response::Builder::new()
724            .header(CACHE_CONTROL, "public,")
725            .header("cache-Control", "max-age=10000")
726            .body(())
727            .unwrap()
728            .into_parts();
729        let cc = CacheControl::from_resp_headers(&parts).unwrap();
730
731        assert!(cc.public());
732        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
733    }
734
735    #[test]
736    fn test_recognizes_semicolons_as_delimiters() {
737        let resp = build_response(CACHE_CONTROL, "public; max-age=0");
738        let cc = CacheControl::from_resp_headers(&resp).unwrap();
739
740        assert!(cc.public());
741        assert_eq!(cc.max_age().unwrap().unwrap(), 0);
742    }
743
744    #[test]
745    fn test_unknown_directives() {
746        let resp = build_response(CACHE_CONTROL, "public,random1=random2, rand3=\"\"");
747        let cc = CacheControl::from_resp_headers(&resp).unwrap();
748        let mut directive_iter = cc.directives.iter();
749
750        let first = directive_iter.next().unwrap();
751        assert_eq!(first.0, &"public");
752        assert!(first.1.is_none());
753
754        let second = directive_iter.next().unwrap();
755        assert_eq!(second.0, &"random1");
756        assert_eq!(second.1.as_ref().unwrap().0, "random2".as_bytes());
757
758        let third = directive_iter.next().unwrap();
759        assert_eq!(third.0, &"rand3");
760        assert_eq!(third.1.as_ref().unwrap().0, "\"\"".as_bytes());
761
762        assert!(directive_iter.next().is_none());
763    }
764
765    #[test]
766    fn test_case_insensitive_directive_keys() {
767        let resp = build_response(
768            CACHE_CONTROL,
769            "Public=\"something\", mAx-AGe=\"10000\", foo=cRaZyCaSe, bAr=\"inQuotes\"",
770        );
771        let cc = CacheControl::from_resp_headers(&resp).unwrap();
772
773        assert!(cc.public());
774        assert_eq!(cc.max_age().unwrap().unwrap(), 10000);
775
776        let mut directive_iter = cc.directives.iter();
777        let first = directive_iter.next().unwrap();
778        assert_eq!(first.0, &"public");
779        assert_eq!(first.1.as_ref().unwrap().0, "\"something\"".as_bytes());
780
781        let second = directive_iter.next().unwrap();
782        assert_eq!(second.0, &"max-age");
783        assert_eq!(second.1.as_ref().unwrap().0, "\"10000\"".as_bytes());
784
785        // values are still stored with casing
786        let third = directive_iter.next().unwrap();
787        assert_eq!(third.0, &"foo");
788        assert_eq!(third.1.as_ref().unwrap().0, "cRaZyCaSe".as_bytes());
789
790        let fourth = directive_iter.next().unwrap();
791        assert_eq!(fourth.0, &"bar");
792        assert_eq!(fourth.1.as_ref().unwrap().0, "\"inQuotes\"".as_bytes());
793
794        assert!(directive_iter.next().is_none());
795    }
796
797    #[test]
798    fn test_non_ascii() {
799        let resp = build_response(CACHE_CONTROL, "püblic=💖, max-age=\"💯\"");
800        let cc = CacheControl::from_resp_headers(&resp).unwrap();
801
802        // Not considered valid registered directive keys / values
803        assert!(!cc.public());
804        assert_eq!(
805            cc.max_age().unwrap_err().context.unwrap().to_string(),
806            "could not parse value as u32"
807        );
808
809        let mut directive_iter = cc.directives.iter();
810        let first = directive_iter.next().unwrap();
811        assert_eq!(first.0, &"püblic");
812        assert_eq!(first.1.as_ref().unwrap().0, "💖".as_bytes());
813
814        let second = directive_iter.next().unwrap();
815        assert_eq!(second.0, &"max-age");
816        assert_eq!(second.1.as_ref().unwrap().0, "\"💯\"".as_bytes());
817
818        assert!(directive_iter.next().is_none());
819    }
820
821    #[test]
822    fn test_non_utf8_key() {
823        let mut resp = response::Builder::new().body(()).unwrap();
824        resp.headers_mut().insert(
825            CACHE_CONTROL,
826            HeaderValue::from_bytes(b"bar\xFF=\"baz\", a=b").unwrap(),
827        );
828        let (parts, _) = resp.into_parts();
829        let cc = CacheControl::from_resp_headers(&parts).unwrap();
830
831        // invalid bytes for key
832        let mut directive_iter = cc.directives.iter();
833        let first = directive_iter.next().unwrap();
834        assert_eq!(first.0, &"a");
835        assert_eq!(first.1.as_ref().unwrap().0, "b".as_bytes());
836
837        assert!(directive_iter.next().is_none());
838    }
839
840    #[test]
841    fn test_non_utf8_value() {
842        // RFC 7230: 0xFF is part of obs-text and is officially considered a valid octet in quoted-strings
843        let mut resp = response::Builder::new().body(()).unwrap();
844        resp.headers_mut().insert(
845            CACHE_CONTROL,
846            HeaderValue::from_bytes(b"max-age=ba\xFFr, bar=\"baz\xFF\", a=b").unwrap(),
847        );
848        let (parts, _) = resp.into_parts();
849        let cc = CacheControl::from_resp_headers(&parts).unwrap();
850
851        assert_eq!(
852            cc.max_age().unwrap_err().context.unwrap().to_string(),
853            "could not parse value as utf8"
854        );
855
856        let mut directive_iter = cc.directives.iter();
857
858        let first = directive_iter.next().unwrap();
859        assert_eq!(first.0, &"max-age");
860        assert_eq!(first.1.as_ref().unwrap().0, b"ba\xFFr");
861
862        let second = directive_iter.next().unwrap();
863        assert_eq!(second.0, &"bar");
864        assert_eq!(second.1.as_ref().unwrap().0, b"\"baz\xFF\"");
865
866        let third = directive_iter.next().unwrap();
867        assert_eq!(third.0, &"a");
868        assert_eq!(third.1.as_ref().unwrap().0, "b".as_bytes());
869
870        assert!(directive_iter.next().is_none());
871    }
872
873    #[test]
874    fn test_age_overflow() {
875        let resp = build_response(
876            CACHE_CONTROL,
877            "max-age=-99999999999999999999999999, s-maxage=99999999999999999999999999",
878        );
879        let cc = CacheControl::from_resp_headers(&resp).unwrap();
880
881        assert_eq!(
882            cc.s_maxage().unwrap().unwrap(),
883            DELTA_SECONDS_OVERFLOW_VALUE
884        );
885        // negative ages still result in errors even with overflow handling
886        assert_eq!(
887            cc.max_age().unwrap_err().context.unwrap().to_string(),
888            "could not parse value as u32"
889        );
890    }
891
892    #[test]
893    fn test_fresh_sec() {
894        let resp = build_response(CACHE_CONTROL, "");
895        let cc = CacheControl::from_resp_headers(&resp).unwrap();
896        assert!(cc.fresh_duration().is_none());
897
898        let resp = build_response(CACHE_CONTROL, "max-age=12345");
899        let cc = CacheControl::from_resp_headers(&resp).unwrap();
900        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(12345));
901
902        let resp = build_response(CACHE_CONTROL, "max-age=99999,s-maxage=123");
903        let cc = CacheControl::from_resp_headers(&resp).unwrap();
904        // prefer s-maxage over max-age
905        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(123));
906    }
907
908    #[test]
909    fn test_cacheability() {
910        let resp = build_response(CACHE_CONTROL, "");
911        let cc = CacheControl::from_resp_headers(&resp).unwrap();
912        assert_eq!(cc.is_cacheable(), Cacheable::Default);
913
914        // uncacheable
915        let resp = build_response(CACHE_CONTROL, "private, max-age=12345");
916        let cc = CacheControl::from_resp_headers(&resp).unwrap();
917        assert_eq!(cc.is_cacheable(), Cacheable::No);
918
919        let resp = build_response(CACHE_CONTROL, "no-store, max-age=12345");
920        let cc = CacheControl::from_resp_headers(&resp).unwrap();
921        assert_eq!(cc.is_cacheable(), Cacheable::No);
922
923        // cacheable
924        let resp = build_response(CACHE_CONTROL, "public");
925        let cc = CacheControl::from_resp_headers(&resp).unwrap();
926        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
927
928        let resp = build_response(CACHE_CONTROL, "max-age=0");
929        let cc = CacheControl::from_resp_headers(&resp).unwrap();
930        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
931    }
932
933    #[test]
934    fn test_no_cache() {
935        let resp = build_response(CACHE_CONTROL, "no-cache, max-age=12345");
936        let cc = CacheControl::from_resp_headers(&resp).unwrap();
937        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
938        assert_eq!(cc.fresh_duration().unwrap(), Duration::ZERO);
939    }
940
941    #[test]
942    fn test_no_cache_field_names() {
943        let resp = build_response(CACHE_CONTROL, "no-cache=\"set-cookie\", max-age=12345");
944        let cc = CacheControl::from_resp_headers(&resp).unwrap();
945        assert!(!cc.private());
946        assert_eq!(cc.is_cacheable(), Cacheable::Yes);
947        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(12345));
948        let mut field_names = cc.no_cache_field_names().unwrap();
949        assert_eq!(
950            str::from_utf8(field_names.next().unwrap()).unwrap(),
951            "set-cookie"
952        );
953        assert!(field_names.next().is_none());
954
955        let mut resp = response::Builder::new().body(()).unwrap();
956        resp.headers_mut().insert(
957            CACHE_CONTROL,
958            HeaderValue::from_bytes(
959                b"private=\"\", no-cache=\"a\xFF, set-cookie, Baz\x09 , c,d  ,, \"",
960            )
961            .unwrap(),
962        );
963        let (parts, _) = resp.into_parts();
964        let cc = CacheControl::from_resp_headers(&parts).unwrap();
965        let mut field_names = cc.private_field_names().unwrap();
966        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
967        assert!(field_names.next().is_none());
968        let mut field_names = cc.no_cache_field_names().unwrap();
969        assert!(str::from_utf8(field_names.next().unwrap()).is_err());
970        assert_eq!(
971            str::from_utf8(field_names.next().unwrap()).unwrap(),
972            "set-cookie"
973        );
974        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "Baz");
975        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "c");
976        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "d");
977        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
978        assert_eq!(str::from_utf8(field_names.next().unwrap()).unwrap(), "");
979        assert!(field_names.next().is_none());
980    }
981
982    #[test]
983    fn test_strip_private_headers() {
984        let mut resp = ResponseHeader::build(200, None).unwrap();
985        resp.append_header(
986            CACHE_CONTROL,
987            "no-cache=\"x-private-header\", max-age=12345",
988        )
989        .unwrap();
990        resp.append_header("X-Private-Header", "dropped").unwrap();
991
992        let cc = CacheControl::from_resp_headers(&resp).unwrap();
993        cc.strip_private_headers(&mut resp);
994        assert!(!resp.headers.contains_key("X-Private-Header"));
995    }
996
997    #[test]
998    fn test_stale_while_revalidate() {
999        let resp = build_response(CACHE_CONTROL, "max-age=12345, stale-while-revalidate=5");
1000        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1001        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 5);
1002        assert_eq!(
1003            cc.serve_stale_while_revalidate_duration().unwrap(),
1004            Duration::from_secs(5)
1005        );
1006        assert!(cc.serve_stale_if_error_duration().is_none());
1007    }
1008
1009    #[test]
1010    fn test_stale_if_error() {
1011        let resp = build_response(CACHE_CONTROL, "max-age=12345, stale-if-error=3600");
1012        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1013        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 3600);
1014        assert_eq!(
1015            cc.serve_stale_if_error_duration().unwrap(),
1016            Duration::from_secs(3600)
1017        );
1018        assert!(cc.serve_stale_while_revalidate_duration().is_none());
1019    }
1020
1021    #[test]
1022    fn test_must_revalidate() {
1023        let resp = build_response(
1024            CACHE_CONTROL,
1025            "max-age=12345, stale-while-revalidate=60, stale-if-error=30, must-revalidate",
1026        );
1027        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1028        assert!(cc.must_revalidate());
1029        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
1030        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
1031        assert_eq!(
1032            cc.serve_stale_while_revalidate_duration().unwrap(),
1033            Duration::ZERO
1034        );
1035        assert_eq!(cc.serve_stale_if_error_duration().unwrap(), Duration::ZERO);
1036    }
1037
1038    #[test]
1039    fn test_proxy_revalidate() {
1040        let resp = build_response(
1041            CACHE_CONTROL,
1042            "max-age=12345, stale-while-revalidate=60, stale-if-error=30, proxy-revalidate",
1043        );
1044        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1045        assert!(cc.proxy_revalidate());
1046        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
1047        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
1048        assert_eq!(
1049            cc.serve_stale_while_revalidate_duration().unwrap(),
1050            Duration::ZERO
1051        );
1052        assert_eq!(cc.serve_stale_if_error_duration().unwrap(), Duration::ZERO);
1053    }
1054
1055    #[test]
1056    fn test_s_maxage_stale() {
1057        let resp = build_response(
1058            CACHE_CONTROL,
1059            "s-maxage=0, stale-while-revalidate=60, stale-if-error=30",
1060        );
1061        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1062        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
1063        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
1064        assert_eq!(
1065            cc.serve_stale_while_revalidate_duration().unwrap(),
1066            Duration::ZERO
1067        );
1068        assert_eq!(cc.serve_stale_if_error_duration().unwrap(), Duration::ZERO);
1069    }
1070
1071    #[test]
1072    fn test_authorized_request() {
1073        let resp = build_response(CACHE_CONTROL, "max-age=10");
1074        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1075        assert!(!cc.allow_caching_authorized_req());
1076
1077        let resp = build_response(CACHE_CONTROL, "s-maxage=10");
1078        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1079        assert!(cc.allow_caching_authorized_req());
1080
1081        let resp = build_response(CACHE_CONTROL, "public");
1082        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1083        assert!(cc.allow_caching_authorized_req());
1084
1085        let resp = build_response(CACHE_CONTROL, "must-revalidate, max-age=0");
1086        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1087        assert!(cc.allow_caching_authorized_req());
1088
1089        let resp = build_response(CACHE_CONTROL, "");
1090        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1091        assert!(!cc.allow_caching_authorized_req());
1092    }
1093
1094    fn build_request(cc_key: HeaderName, cc_value: &str) -> request::Parts {
1095        let (parts, _) = request::Builder::new()
1096            .header(cc_key, cc_value)
1097            .body(())
1098            .unwrap()
1099            .into_parts();
1100        parts
1101    }
1102
1103    #[test]
1104    fn test_request_only_if_cached() {
1105        let req = build_request(CACHE_CONTROL, "only-if-cached=1");
1106        let cc = CacheControl::from_req_headers(&req).unwrap();
1107        assert!(cc.only_if_cached())
1108    }
1109
1110    #[test]
1111    fn test_parse_as_delta_seconds_floor() {
1112        // Integer values behave identically to the strict parser
1113        let v = DirectiveValue(b"10".to_vec());
1114        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 10);
1115
1116        let v = DirectiveValue(b"\"10\"".to_vec());
1117        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 10);
1118
1119        // Quoted fractional values are unwrapped by parse_as_str and floored.
1120        let v = DirectiveValue(b"\"1.5\"".to_vec());
1121        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 1);
1122
1123        let v = DirectiveValue(b"0".to_vec());
1124        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 0);
1125
1126        // Integer positive overflow is still capped
1127        let v = DirectiveValue(b"99999999999999999999".to_vec());
1128        assert_eq!(
1129            v.parse_as_delta_seconds_floor().unwrap(),
1130            DELTA_SECONDS_OVERFLOW_VALUE
1131        );
1132
1133        // Floats are floored
1134        let v = DirectiveValue(b"1.5".to_vec());
1135        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 1);
1136
1137        let v = DirectiveValue(b"1.9".to_vec());
1138        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 1);
1139
1140        let v = DirectiveValue(b"0.5".to_vec());
1141        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 0);
1142
1143        let v = DirectiveValue(b"3600.0".to_vec());
1144        assert_eq!(v.parse_as_delta_seconds_floor().unwrap(), 3600);
1145
1146        // Float positive overflow is capped
1147        let v = DirectiveValue(b"99999999999.5".to_vec());
1148        assert_eq!(
1149            v.parse_as_delta_seconds_floor().unwrap(),
1150            DELTA_SECONDS_OVERFLOW_VALUE
1151        );
1152
1153        // Negative values are rejected (matches strict behavior)
1154        assert!(DirectiveValue(b"-1".to_vec())
1155            .parse_as_delta_seconds_floor()
1156            .is_err());
1157        assert!(DirectiveValue(b"-1.5".to_vec())
1158            .parse_as_delta_seconds_floor()
1159            .is_err());
1160
1161        // Non-finite / non-numeric values are rejected
1162        assert!(DirectiveValue(b"NaN".to_vec())
1163            .parse_as_delta_seconds_floor()
1164            .is_err());
1165        assert!(DirectiveValue(b"inf".to_vec())
1166            .parse_as_delta_seconds_floor()
1167            .is_err());
1168        assert!(DirectiveValue(b"abc".to_vec())
1169            .parse_as_delta_seconds_floor()
1170            .is_err());
1171
1172        // Non-UTF8 bytes are rejected with the same utf-8 error as the strict parser.
1173        let v = DirectiveValue(b"ba\xFFr".to_vec());
1174        assert_eq!(
1175            v.parse_as_delta_seconds_floor()
1176                .unwrap_err()
1177                .context
1178                .unwrap()
1179                .to_string(),
1180            "could not parse value as utf8",
1181        );
1182    }
1183
1184    #[test]
1185    fn test_cache_control_allow_float_seconds_non_utf8_value() {
1186        // Non-UTF8 bytes inside `max-age` should still produce the utf-8 error
1187        // when the float-permitting flag is on, matching the strict parser.
1188        let mut resp = response::Builder::new().body(()).unwrap();
1189        resp.headers_mut().insert(
1190            CACHE_CONTROL,
1191            HeaderValue::from_bytes(b"max-age=ba\xFFr").unwrap(),
1192        );
1193        let (parts, _) = resp.into_parts();
1194        let cc = CacheControl::from_resp_headers(&parts)
1195            .unwrap()
1196            .with_float_seconds();
1197        assert_eq!(
1198            cc.max_age().unwrap_err().context.unwrap().to_string(),
1199            "could not parse value as utf8",
1200        );
1201    }
1202
1203    #[test]
1204    fn test_cache_control_allow_float_seconds_default_off() {
1205        // Default (strict) parsing: fractional values produce an error, and
1206        // [InterpretCacheControl::fresh_duration] returns None, matching the
1207        // pre-existing behavior.
1208        let resp = build_response(CACHE_CONTROL, "max-age=10.7");
1209        let cc = CacheControl::from_resp_headers(&resp).unwrap();
1210        assert!(!cc.allow_float_seconds);
1211        assert!(cc.max_age().is_err());
1212        assert!(cc.fresh_duration().is_none());
1213    }
1214
1215    #[test]
1216    fn test_cache_control_with_float_seconds() {
1217        // `max-age` with a fractional value is floored when the flag is on.
1218        let resp = build_response(CACHE_CONTROL, "max-age=10.7");
1219        let cc = CacheControl::from_resp_headers(&resp)
1220            .unwrap()
1221            .with_float_seconds();
1222        assert!(cc.allow_float_seconds);
1223        assert_eq!(cc.max_age().unwrap().unwrap(), 10);
1224        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(10));
1225
1226        // `s-maxage` still wins over `max-age` and is also floored.
1227        let resp = build_response(CACHE_CONTROL, "s-maxage=3600.99, max-age=1800");
1228        let cc = CacheControl::from_resp_headers(&resp)
1229            .unwrap()
1230            .with_float_seconds();
1231        assert_eq!(cc.s_maxage().unwrap().unwrap(), 3600);
1232        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(3600));
1233
1234        // `stale-while-revalidate` and `stale-if-error` also pick up flooring.
1235        let resp = build_response(
1236            CACHE_CONTROL,
1237            "max-age=10, stale-while-revalidate=60.5, stale-if-error=30.9",
1238        );
1239        let cc = CacheControl::from_resp_headers(&resp)
1240            .unwrap()
1241            .with_float_seconds();
1242        assert_eq!(cc.stale_while_revalidate().unwrap().unwrap(), 60);
1243        assert_eq!(cc.stale_if_error().unwrap().unwrap(), 30);
1244        assert_eq!(
1245            cc.serve_stale_while_revalidate_duration().unwrap(),
1246            Duration::from_secs(60)
1247        );
1248        assert_eq!(
1249            cc.serve_stale_if_error_duration().unwrap(),
1250            Duration::from_secs(30)
1251        );
1252
1253        // Integer values are unaffected when the flag is on.
1254        let resp = build_response(CACHE_CONTROL, "max-age=12345");
1255        let cc = CacheControl::from_resp_headers(&resp)
1256            .unwrap()
1257            .with_float_seconds();
1258        assert_eq!(cc.fresh_duration().unwrap(), Duration::from_secs(12345));
1259
1260        // Invalid (non-numeric, negative) values still fail to parse under the flag.
1261        let resp = build_response(CACHE_CONTROL, "max-age=abc");
1262        let cc = CacheControl::from_resp_headers(&resp)
1263            .unwrap()
1264            .with_float_seconds();
1265        assert!(cc.max_age().is_err());
1266
1267        let resp = build_response(CACHE_CONTROL, "max-age=-1.5");
1268        let cc = CacheControl::from_resp_headers(&resp)
1269            .unwrap()
1270            .with_float_seconds();
1271        assert!(cc.max_age().is_err());
1272    }
1273}