pingora_proxy/
proxy_purge.rs

1// Copyright 2024 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> HttpProxy<SV> {
63    pub(crate) async fn proxy_purge(
64        &self,
65        session: &mut Session,
66        ctx: &mut SV::CTX,
67    ) -> Option<(bool, Option<Box<Error>>)>
68    where
69        SV: ProxyHttp + Send + Sync,
70        SV::CTX: Send + Sync,
71    {
72        let purge_status = if session.cache.enabled() {
73            match session.cache.purge().await {
74                Ok(found) => {
75                    if found {
76                        PurgeStatus::Found
77                    } else {
78                        PurgeStatus::NotFound
79                    }
80                }
81                Err(e) => {
82                    session.cache.disable(NoCacheReason::StorageError);
83                    warn!(
84                        "Fail to purge cache: {e}, {}",
85                        self.inner.request_summary(session, ctx)
86                    );
87                    PurgeStatus::Error(e)
88                }
89            }
90        } else {
91            // cache was not enabled
92            PurgeStatus::NoCache
93        };
94
95        let mut purge_resp = purge_response(&purge_status);
96        if let Err(e) =
97            self.inner
98                .purge_response_filter(session, ctx, purge_status, &mut purge_resp)
99        {
100            error!(
101                "Failed purge response filter: {e}, {}",
102                self.inner.request_summary(session, ctx)
103            );
104            purge_resp = Cow::Borrowed(&*INTERNAL_ERROR)
105        }
106
107        let write_result = match purge_resp {
108            Cow::Borrowed(r) => session.as_mut().write_response_header_ref(r).await,
109            Cow::Owned(r) => session.as_mut().write_response_header(Box::new(r)).await,
110        };
111        let (reuse, err) = match write_result {
112            Ok(_) => (true, None),
113            // dirty, not reusable
114            Err(e) => {
115                let e = e.into_down();
116                error!(
117                    "Failed to send purge response: {e}, {}",
118                    self.inner.request_summary(session, ctx)
119                );
120                (false, Some(e))
121            }
122        };
123        Some((reuse, err))
124    }
125}