Skip to main content

pingora_cache/
filters.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//! Utility functions to help process HTTP headers for caching
16
17use super::*;
18use crate::cache_control::{CacheControl, Cacheable, InterpretCacheControl};
19use crate::RespCacheable::*;
20
21use cache_control::DELTA_SECONDS_OVERFLOW_VALUE;
22use http::{header, HeaderValue};
23use httpdate::HttpDate;
24use log::debug;
25use pingora_http::RequestHeader;
26
27/// Decide if the request can be cacheable
28pub fn request_cacheable(req_header: &ReqHeader) -> bool {
29    // TODO: the check is incomplete
30    matches!(req_header.method, Method::GET | Method::HEAD)
31}
32
33/// Decide if the response is cacheable.
34///
35/// `cache_control` is the parsed [CacheControl] from the response header. It is a standalone
36/// argument so that caller has the flexibility to choose to use, change or ignore it.
37pub fn resp_cacheable(
38    cache_control: Option<&CacheControl>,
39    mut resp_header: ResponseHeader,
40    authorization_present: bool,
41    defaults: &CacheMetaDefaults,
42) -> RespCacheable {
43    let now = SystemTime::now();
44    let expire_time = calculate_fresh_until(
45        now,
46        cache_control,
47        &resp_header,
48        authorization_present,
49        defaults,
50    );
51    if let Some(fresh_until) = expire_time {
52        let (stale_while_revalidate_duration, stale_if_error_duration) =
53            calculate_serve_stale_durations(cache_control, defaults);
54
55        if let Some(cc) = cache_control {
56            cc.strip_private_headers(&mut resp_header);
57        }
58        return Cacheable(CacheMeta::new(
59            fresh_until,
60            now,
61            stale_while_revalidate_duration,
62            stale_if_error_duration,
63            resp_header,
64        ));
65    }
66    Uncacheable(NoCacheReason::OriginNotCache)
67}
68
69/// Calculate the [SystemTime] at which the asset expires
70///
71/// Return None when not cacheable.
72pub fn calculate_fresh_until(
73    now: SystemTime,
74    cache_control: Option<&CacheControl>,
75    resp_header: &RespHeader,
76    authorization_present: bool,
77    defaults: &CacheMetaDefaults,
78) -> Option<SystemTime> {
79    fn freshness_ttl_to_time(now: SystemTime, fresh: Duration) -> Option<SystemTime> {
80        if fresh.is_zero() {
81            // ensure that the response is treated as stale
82            now.checked_sub(Duration::from_secs(1))
83        } else {
84            now.checked_add(fresh)
85        }
86    }
87
88    // A request with Authorization is normally not cacheable, unless Cache-Control allows it
89    if authorization_present {
90        let uncacheable = cache_control
91            .as_ref()
92            .is_none_or(|cc| !cc.allow_caching_authorized_req());
93        if uncacheable {
94            return None;
95        }
96    }
97
98    let uncacheable = cache_control
99        .as_ref()
100        .is_some_and(|cc| cc.is_cacheable() == Cacheable::No);
101    if uncacheable {
102        return None;
103    }
104
105    // For TTL check cache-control first, then expires header, then defaults
106    cache_control
107        .and_then(|cc| {
108            cc.fresh_duration()
109                .and_then(|ttl| freshness_ttl_to_time(now, ttl))
110        })
111        .or_else(|| calculate_expires_header_time(resp_header))
112        .or_else(|| {
113            defaults
114                .fresh_sec(resp_header.status)
115                .and_then(|ttl| freshness_ttl_to_time(now, ttl))
116        })
117}
118
119/// Calculate the expire time from the `Expires` header only
120pub fn calculate_expires_header_time(resp_header: &RespHeader) -> Option<SystemTime> {
121    // according to RFC 7234:
122    // https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.1
123    // - treat multiple expires headers as invalid
124    // https://datatracker.ietf.org/doc/html/rfc7234#section-5.3
125    // - "MUST interpret invalid date formats... as representing a time in the past"
126    fn parse_expires_value(expires_value: &HeaderValue) -> Option<SystemTime> {
127        let expires = expires_value.to_str().ok()?;
128        Some(SystemTime::from(
129            expires
130                .parse::<HttpDate>()
131                .map_err(|e| debug!("Invalid HttpDate in Expires: {}, error: {}", expires, e))
132                .ok()?,
133        ))
134    }
135
136    let mut expires_iter = resp_header.headers.get_all("expires").iter();
137    let expires_header = expires_iter.next();
138    if expires_header.is_none() || expires_iter.next().is_some() {
139        return None;
140    }
141    parse_expires_value(expires_header.unwrap()).or(Some(SystemTime::UNIX_EPOCH))
142}
143
144/// Calculates stale-while-revalidate and stale-if-error seconds from Cache-Control or the [CacheMetaDefaults].
145pub fn calculate_serve_stale_durations(
146    cache_control: Option<&impl InterpretCacheControl>,
147    defaults: &CacheMetaDefaults,
148) -> (u32, u32) {
149    let serve_stale_while_revalidate = cache_control
150        .and_then(|cc| cc.serve_stale_while_revalidate_duration())
151        .unwrap_or_else(|| Duration::from_secs(defaults.serve_stale_while_revalidate_sec() as u64));
152    let serve_stale_if_error = cache_control
153        .and_then(|cc| cc.serve_stale_if_error_duration())
154        .unwrap_or_else(|| Duration::from_secs(defaults.serve_stale_if_error_sec() as u64));
155    (
156        serve_stale_while_revalidate
157            .as_secs()
158            .try_into()
159            .unwrap_or(DELTA_SECONDS_OVERFLOW_VALUE),
160        serve_stale_if_error
161            .as_secs()
162            .try_into()
163            .unwrap_or(DELTA_SECONDS_OVERFLOW_VALUE),
164    )
165}
166
167/// Filters to run when sending requests to upstream
168pub mod upstream {
169    use super::*;
170
171    /// Adjust the request header for cacheable requests
172    ///
173    /// This filter does the following in order to fetch the entire response to cache
174    /// - Convert HEAD to GET
175    /// - `If-*` headers are removed
176    /// - `Range` header is removed
177    ///
178    /// When `meta` is set, this function will inject `If-modified-since` according to the `Last-Modified` header
179    /// and inject `If-none-match` according to `Etag` header
180    pub fn request_filter(req: &mut RequestHeader, meta: Option<&CacheMeta>) {
181        // change HEAD to GET, HEAD itself is not semantically cacheable
182        if req.method == Method::HEAD {
183            req.set_method(Method::GET);
184        }
185
186        // remove downstream precondition headers https://datatracker.ietf.org/doc/html/rfc7232#section-3
187        // we'd like to cache the 200 not the 304
188        req.remove_header(&header::IF_MATCH);
189        req.remove_header(&header::IF_NONE_MATCH);
190        req.remove_header(&header::IF_MODIFIED_SINCE);
191        req.remove_header(&header::IF_UNMODIFIED_SINCE);
192        // see below range header
193        req.remove_header(&header::IF_RANGE);
194
195        // remove downstream range header as we'd like to cache the entire response (this might change in the future)
196        req.remove_header(&header::RANGE);
197
198        // we have a presumably staled response already, add precondition headers for revalidation
199        if let Some(m) = meta {
200            // rfc7232: "SHOULD send both validators in cache validation" but
201            // there have been weird cases that an origin has matching etag but not Last-Modified
202            if let Some(since) = m.headers().get(&header::LAST_MODIFIED) {
203                req.insert_header(header::IF_MODIFIED_SINCE, since).unwrap();
204            }
205            if let Some(etag) = m.headers().get(&header::ETAG) {
206                req.insert_header(header::IF_NONE_MATCH, etag).unwrap();
207            }
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::RespCacheable::Cacheable;
216    use http::header::{HeaderName, CACHE_CONTROL, EXPIRES, SET_COOKIE};
217    use http::StatusCode;
218    use httpdate::fmt_http_date;
219
220    fn init_log() {
221        let _ = env_logger::builder().is_test(true).try_init();
222    }
223
224    const DEFAULTS: CacheMetaDefaults = CacheMetaDefaults::new(
225        |status| {
226            match status {
227                StatusCode::OK => Some(10),
228                StatusCode::NOT_FOUND => Some(5),
229                StatusCode::PARTIAL_CONTENT => None,
230                _ => Some(1),
231            }
232            .map(Duration::from_secs)
233        },
234        0,
235        DELTA_SECONDS_OVERFLOW_VALUE, /* "infinite" stale-if-error */
236    );
237
238    // Cache nothing, by default
239    const BYPASS_CACHE_DEFAULTS: CacheMetaDefaults = CacheMetaDefaults::new(|_| None, 0, 0);
240
241    fn build_response(status: u16, headers: &[(HeaderName, &str)]) -> ResponseHeader {
242        let mut header = ResponseHeader::build(status, Some(headers.len())).unwrap();
243        for (k, v) in headers {
244            header.append_header(k.to_string(), *v).unwrap();
245        }
246        header
247    }
248
249    fn resp_cacheable_wrapper(
250        resp: ResponseHeader,
251        defaults: &CacheMetaDefaults,
252        authorization_present: bool,
253    ) -> Option<CacheMeta> {
254        if let Cacheable(meta) = resp_cacheable(
255            CacheControl::from_resp_headers(&resp).as_ref(),
256            resp,
257            authorization_present,
258            defaults,
259        ) {
260            Some(meta)
261        } else {
262            None
263        }
264    }
265
266    #[test]
267    fn test_resp_cacheable() {
268        let meta = resp_cacheable_wrapper(
269            build_response(200, &[(CACHE_CONTROL, "max-age=12345")]),
270            &DEFAULTS,
271            false,
272        );
273
274        let meta = meta.unwrap();
275        assert!(meta.is_fresh(SystemTime::now()));
276        assert!(meta.is_fresh(
277            SystemTime::now()
278                .checked_add(Duration::from_secs(12))
279                .unwrap()
280        ),);
281        assert!(!meta.is_fresh(
282            SystemTime::now()
283                .checked_add(Duration::from_secs(12346))
284                .unwrap()
285        ));
286    }
287
288    #[test]
289    fn test_resp_uncacheable_directives() {
290        let meta = resp_cacheable_wrapper(
291            build_response(200, &[(CACHE_CONTROL, "private, max-age=12345")]),
292            &DEFAULTS,
293            false,
294        );
295        assert!(meta.is_none());
296
297        let meta = resp_cacheable_wrapper(
298            build_response(200, &[(CACHE_CONTROL, "no-store, max-age=12345")]),
299            &DEFAULTS,
300            false,
301        );
302        assert!(meta.is_none());
303    }
304
305    #[test]
306    fn test_resp_cache_authorization() {
307        let meta = resp_cacheable_wrapper(build_response(200, &[]), &DEFAULTS, true);
308        assert!(meta.is_none());
309
310        let meta = resp_cacheable_wrapper(
311            build_response(200, &[(CACHE_CONTROL, "max-age=10")]),
312            &DEFAULTS,
313            true,
314        );
315        assert!(meta.is_none());
316
317        let meta = resp_cacheable_wrapper(
318            build_response(200, &[(CACHE_CONTROL, "s-maxage=10")]),
319            &DEFAULTS,
320            true,
321        );
322        assert!(meta.unwrap().is_fresh(SystemTime::now()));
323
324        let meta = resp_cacheable_wrapper(
325            build_response(200, &[(CACHE_CONTROL, "public, max-age=10")]),
326            &DEFAULTS,
327            true,
328        );
329        assert!(meta.unwrap().is_fresh(SystemTime::now()));
330
331        let meta = resp_cacheable_wrapper(
332            build_response(200, &[(CACHE_CONTROL, "must-revalidate")]),
333            &DEFAULTS,
334            true,
335        );
336        assert!(meta.unwrap().is_fresh(SystemTime::now()));
337    }
338
339    #[test]
340    fn test_resp_zero_max_age() {
341        let meta = resp_cacheable_wrapper(
342            build_response(200, &[(CACHE_CONTROL, "max-age=0, public")]),
343            &DEFAULTS,
344            false,
345        );
346
347        // cacheable, but needs revalidation
348        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
349    }
350
351    #[test]
352    fn test_resp_expires() {
353        let five_sec_time = SystemTime::now()
354            .checked_add(Duration::from_secs(5))
355            .unwrap();
356
357        // future expires is cacheable
358        let meta = resp_cacheable_wrapper(
359            build_response(200, &[(EXPIRES, &fmt_http_date(five_sec_time))]),
360            &DEFAULTS,
361            false,
362        );
363
364        let meta = meta.unwrap();
365        assert!(meta.is_fresh(SystemTime::now()));
366        assert!(!meta.is_fresh(
367            SystemTime::now()
368                .checked_add(Duration::from_secs(6))
369                .unwrap()
370        ));
371
372        // even on default uncacheable statuses
373        let meta = resp_cacheable_wrapper(
374            build_response(206, &[(EXPIRES, &fmt_http_date(five_sec_time))]),
375            &DEFAULTS,
376            false,
377        );
378        assert!(meta.is_some());
379    }
380
381    #[test]
382    fn test_resp_past_expires() {
383        // cacheable, but expired
384        let meta = resp_cacheable_wrapper(
385            build_response(200, &[(EXPIRES, "Fri, 15 May 2015 15:34:21 GMT")]),
386            &BYPASS_CACHE_DEFAULTS,
387            false,
388        );
389        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
390    }
391
392    #[test]
393    fn test_resp_nonstandard_expires() {
394        // init log to allow inspecting warnings
395        init_log();
396
397        // invalid cases, according to parser
398        // (but should be stale according to RFC)
399        let meta = resp_cacheable_wrapper(
400            build_response(200, &[(EXPIRES, "Mon, 13 Feb 0002 12:00:00 GMT")]),
401            &BYPASS_CACHE_DEFAULTS,
402            false,
403        );
404        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
405
406        let meta = resp_cacheable_wrapper(
407            build_response(200, &[(EXPIRES, "Fri, 01 Dec 99999 16:00:00 GMT")]),
408            &BYPASS_CACHE_DEFAULTS,
409            false,
410        );
411        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
412
413        let meta = resp_cacheable_wrapper(
414            build_response(200, &[(EXPIRES, "0")]),
415            &BYPASS_CACHE_DEFAULTS,
416            false,
417        );
418        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
419    }
420
421    #[test]
422    fn test_resp_multiple_expires() {
423        let five_sec_time = SystemTime::now()
424            .checked_add(Duration::from_secs(5))
425            .unwrap();
426        let ten_sec_time = SystemTime::now()
427            .checked_add(Duration::from_secs(10))
428            .unwrap();
429
430        // multiple expires = uncacheable
431        let meta = resp_cacheable_wrapper(
432            build_response(
433                200,
434                &[
435                    (EXPIRES, &fmt_http_date(five_sec_time)),
436                    (EXPIRES, &fmt_http_date(ten_sec_time)),
437                ],
438            ),
439            &BYPASS_CACHE_DEFAULTS,
440            false,
441        );
442        assert!(meta.is_none());
443
444        // unless the default is cacheable
445        let meta = resp_cacheable_wrapper(
446            build_response(
447                200,
448                &[
449                    (EXPIRES, &fmt_http_date(five_sec_time)),
450                    (EXPIRES, &fmt_http_date(ten_sec_time)),
451                ],
452            ),
453            &DEFAULTS,
454            false,
455        );
456        assert!(meta.is_some());
457    }
458
459    #[test]
460    fn test_resp_cache_control_with_expires() {
461        let five_sec_time = SystemTime::now()
462            .checked_add(Duration::from_secs(5))
463            .unwrap();
464        // cache-control takes precedence over expires
465        let meta = resp_cacheable_wrapper(
466            build_response(
467                200,
468                &[
469                    (EXPIRES, &fmt_http_date(five_sec_time)),
470                    (CACHE_CONTROL, "max-age=0"),
471                ],
472            ),
473            &DEFAULTS,
474            false,
475        );
476        assert!(!meta.unwrap().is_fresh(SystemTime::now()));
477    }
478
479    #[test]
480    fn test_resp_stale_while_revalidate() {
481        // respect defaults
482        let meta = resp_cacheable_wrapper(
483            build_response(200, &[(CACHE_CONTROL, "max-age=10")]),
484            &DEFAULTS,
485            false,
486        );
487
488        let meta = meta.unwrap();
489        let eleven_sec_time = SystemTime::now()
490            .checked_add(Duration::from_secs(11))
491            .unwrap();
492        assert!(!meta.is_fresh(eleven_sec_time));
493        assert!(!meta.serve_stale_while_revalidate(SystemTime::now()));
494        assert!(!meta.serve_stale_while_revalidate(eleven_sec_time));
495
496        // override with stale-while-revalidate
497        let meta = resp_cacheable_wrapper(
498            build_response(
499                200,
500                &[(CACHE_CONTROL, "max-age=10, stale-while-revalidate=5")],
501            ),
502            &DEFAULTS,
503            false,
504        );
505
506        let meta = meta.unwrap();
507        let eleven_sec_time = SystemTime::now()
508            .checked_add(Duration::from_secs(11))
509            .unwrap();
510        let sixteen_sec_time = SystemTime::now()
511            .checked_add(Duration::from_secs(16))
512            .unwrap();
513        assert!(!meta.is_fresh(eleven_sec_time));
514        assert!(meta.serve_stale_while_revalidate(eleven_sec_time));
515        assert!(!meta.serve_stale_while_revalidate(sixteen_sec_time));
516    }
517
518    #[test]
519    fn test_resp_stale_if_error() {
520        // respect defaults
521        let meta = resp_cacheable_wrapper(
522            build_response(200, &[(CACHE_CONTROL, "max-age=10")]),
523            &DEFAULTS,
524            false,
525        );
526
527        let meta = meta.unwrap();
528        let fifty_years_time = SystemTime::now()
529            .checked_add(Duration::from_secs(86400 * 365 * 50))
530            .unwrap();
531        assert!(!meta.is_fresh(fifty_years_time));
532        assert!(meta.serve_stale_if_error(fifty_years_time));
533
534        // override with stale-if-error
535        let meta = resp_cacheable_wrapper(
536            build_response(
537                200,
538                &[(
539                    CACHE_CONTROL,
540                    "max-age=10, stale-while-revalidate=5, stale-if-error=60",
541                )],
542            ),
543            &DEFAULTS,
544            false,
545        );
546
547        let meta = meta.unwrap();
548        let eleven_sec_time = SystemTime::now()
549            .checked_add(Duration::from_secs(11))
550            .unwrap();
551        let seventy_sec_time = SystemTime::now()
552            .checked_add(Duration::from_secs(70))
553            .unwrap();
554        assert!(!meta.is_fresh(eleven_sec_time));
555        assert!(meta.serve_stale_if_error(SystemTime::now()));
556        assert!(meta.serve_stale_if_error(eleven_sec_time));
557        assert!(!meta.serve_stale_if_error(seventy_sec_time));
558
559        // never serve stale
560        let meta = resp_cacheable_wrapper(
561            build_response(200, &[(CACHE_CONTROL, "max-age=10, stale-if-error=0")]),
562            &DEFAULTS,
563            false,
564        );
565
566        let meta = meta.unwrap();
567        let eleven_sec_time = SystemTime::now()
568            .checked_add(Duration::from_secs(11))
569            .unwrap();
570        assert!(!meta.is_fresh(eleven_sec_time));
571        assert!(!meta.serve_stale_if_error(eleven_sec_time));
572    }
573
574    #[test]
575    fn test_resp_status_cache_defaults() {
576        // 200 response
577        let meta = resp_cacheable_wrapper(build_response(200, &[]), &DEFAULTS, false);
578        assert!(meta.is_some());
579
580        let meta = meta.unwrap();
581        assert!(meta.is_fresh(
582            SystemTime::now()
583                .checked_add(Duration::from_secs(9))
584                .unwrap()
585        ));
586        assert!(!meta.is_fresh(
587            SystemTime::now()
588                .checked_add(Duration::from_secs(11))
589                .unwrap()
590        ));
591
592        // 404 response, different ttl
593        let meta = resp_cacheable_wrapper(build_response(404, &[]), &DEFAULTS, false);
594        assert!(meta.is_some());
595
596        let meta = meta.unwrap();
597        assert!(meta.is_fresh(
598            SystemTime::now()
599                .checked_add(Duration::from_secs(4))
600                .unwrap()
601        ));
602        assert!(!meta.is_fresh(
603            SystemTime::now()
604                .checked_add(Duration::from_secs(6))
605                .unwrap()
606        ));
607
608        // 206 marked uncacheable (no cache TTL)
609        let meta = resp_cacheable_wrapper(build_response(206, &[]), &DEFAULTS, false);
610        assert!(meta.is_none());
611
612        // default uncacheable status with explicit Cache-Control is cacheable
613        let meta = resp_cacheable_wrapper(
614            build_response(206, &[(CACHE_CONTROL, "public, max-age=10")]),
615            &DEFAULTS,
616            false,
617        );
618        assert!(meta.is_some());
619
620        let meta = meta.unwrap();
621        assert!(meta.is_fresh(
622            SystemTime::now()
623                .checked_add(Duration::from_secs(9))
624                .unwrap()
625        ));
626        assert!(!meta.is_fresh(
627            SystemTime::now()
628                .checked_add(Duration::from_secs(11))
629                .unwrap()
630        ));
631
632        // 416 matches any status
633        let meta = resp_cacheable_wrapper(build_response(416, &[]), &DEFAULTS, false);
634        assert!(meta.is_some());
635
636        let meta = meta.unwrap();
637        assert!(meta.is_fresh(SystemTime::now()));
638        assert!(!meta.is_fresh(
639            SystemTime::now()
640                .checked_add(Duration::from_secs(2))
641                .unwrap()
642        ));
643    }
644
645    #[test]
646    fn test_resp_cache_no_cache_fields() {
647        // check #field-names are stripped from the cache header
648        let meta = resp_cacheable_wrapper(
649            build_response(
650                200,
651                &[
652                    (SET_COOKIE, "my-cookie"),
653                    (CACHE_CONTROL, "private=\"something\", max-age=10"),
654                    (HeaderName::from_bytes(b"Something").unwrap(), "foo"),
655                ],
656            ),
657            &DEFAULTS,
658            false,
659        );
660        let meta = meta.unwrap();
661        assert!(meta.headers().contains_key(SET_COOKIE));
662        assert!(!meta.headers().contains_key("Something"));
663
664        let meta = resp_cacheable_wrapper(
665            build_response(
666                200,
667                &[
668                    (SET_COOKIE, "my-cookie"),
669                    (
670                        CACHE_CONTROL,
671                        "max-age=0, no-cache=\"meta1, SeT-Cookie ,meta2\"",
672                    ),
673                    (HeaderName::from_bytes(b"meta1").unwrap(), "foo"),
674                ],
675            ),
676            &DEFAULTS,
677            false,
678        );
679        let meta = meta.unwrap();
680        assert!(!meta.headers().contains_key(SET_COOKIE));
681        assert!(!meta.headers().contains_key("meta1"));
682    }
683}