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