Skip to main content

pingora_proxy/
proxy_purge.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
15use super::*;
16use pingora_cache::PurgeAction;
17use pingora_core::protocols::http::error_resp;
18use std::borrow::Cow;
19
20#[derive(Debug)]
21pub enum PurgeStatus {
22    /// Cache was not enabled, purge ineffectual.
23    NoCache,
24    /// Asset was found in cache (and presumably purged or being purged).
25    Found,
26    /// Asset was not found in cache.
27    NotFound,
28    /// Cache returned a purge error.
29    /// Contains causing error in case it should affect the downstream response.
30    Error(Box<Error>),
31}
32
33// Return a canned response to a purge request, based on whether the cache had the asset or not
34// (or otherwise returned an error).
35fn purge_response(purge_status: &PurgeStatus) -> Cow<'static, ResponseHeader> {
36    let resp = match purge_status {
37        PurgeStatus::NoCache => &*NOT_PURGEABLE,
38        PurgeStatus::Found => &*OK,
39        PurgeStatus::NotFound => &*NOT_FOUND,
40        PurgeStatus::Error(ref _e) => &*INTERNAL_ERROR,
41    };
42    Cow::Borrowed(resp)
43}
44
45fn gen_purge_response(code: u16) -> ResponseHeader {
46    let mut resp = ResponseHeader::build(code, Some(3)).unwrap();
47    resp.insert_header(header::SERVER, &SERVER_NAME[..])
48        .unwrap();
49    resp.insert_header(header::CONTENT_LENGTH, 0).unwrap();
50    resp.insert_header(header::CACHE_CONTROL, "private, no-store")
51        .unwrap();
52    // TODO more headers?
53    resp
54}
55
56static OK: Lazy<ResponseHeader> = Lazy::new(|| gen_purge_response(200));
57static NOT_FOUND: Lazy<ResponseHeader> = Lazy::new(|| gen_purge_response(404));
58// for when purge is sent to uncacheable assets
59static NOT_PURGEABLE: Lazy<ResponseHeader> = Lazy::new(|| gen_purge_response(405));
60// on cache storage or proxy error
61static INTERNAL_ERROR: Lazy<ResponseHeader> = Lazy::new(|| error_resp::gen_error_response(500));
62
63impl<SV, C> HttpProxy<SV, C>
64where
65    C: custom::Connector,
66{
67    pub(crate) async fn proxy_purge(
68        &self,
69        session: &mut Session,
70        ctx: &mut SV::CTX,
71    ) -> Option<(bool, Option<Box<Error>>)>
72    where
73        SV: ProxyHttp + Send + Sync,
74        SV::CTX: Send + Sync,
75    {
76        let purge_status = if session.cache.enabled() {
77            let purged = match self.inner.purge_action(session, ctx) {
78                PurgeAction::Delete => session.cache.purge().await,
79                PurgeAction::Expire => session.cache.expire().await,
80            };
81            match purged {
82                Ok(found) => {
83                    if found {
84                        PurgeStatus::Found
85                    } else {
86                        PurgeStatus::NotFound
87                    }
88                }
89                Err(e) => {
90                    session.cache.disable(NoCacheReason::StorageError);
91                    warn!(
92                        "Fail to purge cache: {e}, {}",
93                        self.inner.request_summary(session, ctx)
94                    );
95                    PurgeStatus::Error(e)
96                }
97            }
98        } else {
99            // cache was not enabled
100            PurgeStatus::NoCache
101        };
102
103        let mut purge_resp = purge_response(&purge_status);
104        if let Err(e) =
105            self.inner
106                .purge_response_filter(session, ctx, purge_status, &mut purge_resp)
107        {
108            error!(
109                "Failed purge response filter: {e}, {}",
110                self.inner.request_summary(session, ctx)
111            );
112            purge_resp = Cow::Borrowed(&*INTERNAL_ERROR)
113        }
114
115        let write_result = match purge_resp {
116            Cow::Borrowed(r) => session.as_mut().write_response_header_ref(r).await,
117            Cow::Owned(r) => session.as_mut().write_response_header(Box::new(r)).await,
118        };
119        let (reuse, err) = match write_result {
120            Ok(_) => (true, None),
121            // dirty, not reusable
122            Err(e) => {
123                let e = e.into_down();
124                error!(
125                    "Failed to send purge response: {e}, {}",
126                    self.inner.request_summary(session, ctx)
127                );
128                (false, Some(e))
129            }
130        };
131        Some((reuse, err))
132    }
133}