Skip to main content

pingora_proxy/
proxy_trait.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::{
17    key::HashBinary,
18    CacheKey, CacheMeta, ForcedFreshness, HitHandler,
19    RespCacheable::{self, *},
20};
21use proxy_cache::range_filter::{self};
22use std::time::Duration;
23
24/// The interface to control the HTTP proxy
25///
26/// The methods in [ProxyHttp] are filters/callbacks which will be performed on all requests at their
27/// particular stage (if applicable).
28///
29/// If any of the filters returns [Result::Err], the request will fail, and the error will be logged.
30#[cfg_attr(not(doc_async_trait), async_trait)]
31pub trait ProxyHttp {
32    /// The per request object to share state across the different filters
33    type CTX;
34
35    /// Define how the `ctx` should be created.
36    fn new_ctx(&self) -> Self::CTX;
37
38    /// Define where the proxy should send the request to.
39    ///
40    /// The returned [HttpPeer] contains the information regarding where and how this request should
41    /// be forwarded to.
42    async fn upstream_peer(
43        &self,
44        session: &mut Session,
45        ctx: &mut Self::CTX,
46    ) -> Result<Box<HttpPeer>>;
47
48    /// Set up downstream modules.
49    ///
50    /// In this phase, users can add or configure [HttpModules] before the server starts up.
51    ///
52    /// In the default implementation of this method, [ResponseCompressionBuilder] is added
53    /// and disabled.
54    fn init_downstream_modules(&self, modules: &mut HttpModules) {
55        // Add disabled downstream compression module by default
56        modules.add_module(ResponseCompressionBuilder::enable(0));
57    }
58
59    /// Handle the incoming request.
60    ///
61    /// In this phase, users can parse, validate, rate limit, perform access control and/or
62    /// return a response for this request.
63    ///
64    /// If the user already sent a response to this request, an `Ok(true)` should be returned so that
65    /// the proxy would exit. The proxy continues to the next phases when `Ok(false)` is returned.
66    ///
67    /// By default this filter does nothing and returns `Ok(false)`.
68    async fn request_filter(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool>
69    where
70        Self::CTX: Send + Sync,
71    {
72        Ok(false)
73    }
74
75    /// Handle the incoming request before any downstream module is executed.
76    ///
77    /// This function is similar to [Self::request_filter()] but executes before any other logic,
78    /// including downstream module logic. The main purpose of this function is to provide finer
79    /// grained control of the behavior of the modules.
80    ///
81    /// Note that because this function is executed before any module that might provide access
82    /// control or rate limiting, logic should stay in request_filter() if it can in order to be
83    /// protected by said modules.
84    async fn early_request_filter(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result<()>
85    where
86        Self::CTX: Send + Sync,
87    {
88        Ok(())
89    }
90
91    /// Returns whether this session is allowed to spawn subrequests.
92    ///
93    /// This function is checked after [Self::early_request_filter] to allow that filter to configure
94    /// this if required. This will also run for subrequests themselves, which may allowed to spawn
95    /// their own subrequests.
96    ///
97    /// Note that this doesn't prevent subrequests from being spawned based on the session by proxy
98    /// core functionality, e.g. background cache revalidation requires spawning subrequests.
99    fn allow_spawning_subrequest(&self, _session: &Session, _ctx: &Self::CTX) -> bool
100    where
101        Self::CTX: Send + Sync,
102    {
103        false
104    }
105
106    /// Handle the incoming request body.
107    ///
108    /// This function will be called every time a piece of request body is received. The `body` is
109    /// **not the entire request body**.
110    ///
111    /// The async nature of this function allows to throttle the upload speed and/or executing
112    /// heavy computation logic such as WAF rules on offloaded threads without blocking the threads
113    /// who process the requests themselves.
114    async fn request_body_filter(
115        &self,
116        _session: &mut Session,
117        _body: &mut Option<Bytes>,
118        _end_of_stream: bool,
119        _ctx: &mut Self::CTX,
120    ) -> Result<()>
121    where
122        Self::CTX: Send + Sync,
123    {
124        Ok(())
125    }
126
127    /// This filter decides if the request is cacheable and what cache backend to use
128    ///
129    /// The caller can interact with `Session.cache` to enable caching.
130    ///
131    /// By default this filter does nothing which effectively disables caching.
132    // Ideally only session.cache should be modified, TODO: reflect that in this interface
133    fn request_cache_filter(&self, _session: &mut Session, _ctx: &mut Self::CTX) -> Result<()>
134    where
135        Self::CTX: Send + Sync,
136    {
137        Ok(())
138    }
139
140    /// This callback generates the cache key.
141    ///
142    /// This callback is called only when cache is enabled for this request.
143    ///
144    /// There is no sensible default cache key for all proxy applications. The
145    /// correct key depends on which request properties affect upstream responses
146    /// (e.g. `Vary` headers, custom request filters that modify the origin host).
147    /// Getting this wrong leads to cache poisoning.
148    ///
149    /// See `pingora-proxy/tests/utils/server_utils.rs` for a minimal (not
150    /// production-ready) reference implementation.
151    ///
152    /// # Panics
153    ///
154    /// The default implementation panics. You **must** override this method when
155    /// caching is enabled.
156    fn cache_key_callback(&self, _session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
157        unimplemented!("cache_key_callback must be implemented when caching is enabled")
158    }
159
160    /// This callback is invoked when a cacheable response is ready to be admitted to cache.
161    fn cache_miss(&self, session: &mut Session, _ctx: &mut Self::CTX) {
162        session.cache.cache_miss();
163    }
164
165    /// This filter is called after a successful cache lookup and before the
166    /// cache asset is ready to be used.
167    ///
168    /// This filter allows the user to log or force invalidate the asset, or
169    /// to adjust the body reader associated with the cache hit.
170    /// This also runs on stale hit assets (for which `is_fresh` is false).
171    ///
172    /// The value returned indicates if the force invalidation should be used,
173    /// and which kind. Returning `None` indicates no forced invalidation
174    async fn cache_hit_filter(
175        &self,
176        _session: &mut Session,
177        _meta: &CacheMeta,
178        _hit_handler: &mut HitHandler,
179        _is_fresh: bool,
180        _ctx: &mut Self::CTX,
181    ) -> Result<Option<ForcedFreshness>>
182    where
183        Self::CTX: Send + Sync,
184    {
185        Ok(None)
186    }
187
188    /// Decide if a request should continue to upstream after not being served from cache.
189    ///
190    /// returns: Ok(true) if the request should continue, Ok(false) if a response was written by the
191    /// callback and the session should be finished, or an error
192    ///
193    /// This filter can be used for deferring checks like rate limiting or access control to when they
194    /// actually needed after cache miss.
195    ///
196    /// By default the session will attempt to be reused after returning Ok(false). It is the
197    /// caller's responsibility to disable keepalive or drain the request body if needed.
198    async fn proxy_upstream_filter(
199        &self,
200        _session: &mut Session,
201        _ctx: &mut Self::CTX,
202    ) -> Result<bool>
203    where
204        Self::CTX: Send + Sync,
205    {
206        Ok(true)
207    }
208
209    /// Decide if the response is cacheable
210    fn response_cache_filter(
211        &self,
212        _session: &Session,
213        _resp: &ResponseHeader,
214        _ctx: &mut Self::CTX,
215    ) -> Result<RespCacheable> {
216        Ok(Uncacheable(NoCacheReason::Custom("default")))
217    }
218
219    /// Decide how to generate cache vary key from both request and response
220    ///
221    /// None means no variance is needed.
222    fn cache_vary_filter(
223        &self,
224        _meta: &CacheMeta,
225        _ctx: &mut Self::CTX,
226        _req: &RequestHeader,
227    ) -> Option<HashBinary> {
228        // default to None for now to disable vary feature
229        None
230    }
231
232    /// Decide if the incoming request's condition _fails_ against the cached response.
233    ///
234    /// Returning `Ok(true)` means that the response does _not_ match against the condition, and
235    /// that the proxy can return `304 Not Modified` downstream.
236    ///
237    /// An example is a conditional GET request with `If-None-Match: "foobar"`. If the cached
238    /// response contains the `ETag: "foobar"`, then the condition fails, and `304 Not Modified`
239    /// should be returned. Else, the condition passes which means the full `200 OK` response must
240    /// be sent.
241    fn cache_not_modified_filter(
242        &self,
243        session: &Session,
244        resp: &ResponseHeader,
245        _ctx: &mut Self::CTX,
246    ) -> Result<bool> {
247        Ok(
248            pingora_core::protocols::http::conditional_filter::not_modified_filter(
249                session.req_header(),
250                resp,
251            ),
252        )
253    }
254
255    /// This filter is called when cache is enabled to determine what byte range to return (in both
256    /// cache hit and miss cases) from the response body. It is only used when caching is enabled,
257    /// otherwise the upstream is responsible for any filtering. It allows users to define the range
258    /// this request is for via its return type `range_filter::RangeType`.
259    ///
260    /// It also allow users to modify the response header accordingly.
261    ///
262    /// The default implementation can handle a single-range as per [RFC7232].
263    ///
264    /// [RFC7232]: https://www.rfc-editor.org/rfc/rfc7232
265    fn range_header_filter(
266        &self,
267        session: &mut Session,
268        resp: &mut ResponseHeader,
269        _ctx: &mut Self::CTX,
270    ) -> range_filter::RangeType {
271        const DEFAULT_MAX_RANGES: Option<usize> = Some(200);
272        proxy_cache::range_filter::range_header_filter(
273            session.req_header(),
274            resp,
275            DEFAULT_MAX_RANGES,
276        )
277    }
278
279    /// Modify the request before it is sent to the upstream
280    ///
281    /// Unlike [Self::request_filter()], this filter allows to change the request headers to send
282    /// to the upstream.
283    async fn upstream_request_filter(
284        &self,
285        _session: &mut Session,
286        _upstream_request: &mut RequestHeader,
287        _ctx: &mut Self::CTX,
288    ) -> Result<()>
289    where
290        Self::CTX: Send + Sync,
291    {
292        Ok(())
293    }
294
295    /// Modify the response header from the upstream
296    ///
297    /// Decide whether to discard an upstream response and retry the request.
298    ///
299    /// Called with the upstream response header *before* any of the response
300    /// reaches downstream, so returning `true` discards it and re-runs the
301    /// request against a freshly selected peer. This is what makes a
302    /// "retry on 502/503" policy expressible: the built-in retry loop only
303    /// re-runs on transport errors, and by the time a status code is known
304    /// there is no error to react to.
305    ///
306    /// Returning `true` is only honoured when a retry is actually possible:
307    ///
308    /// * retries must remain (see the `max_retries` server setting), and
309    /// * the request body must have been buffered in full for replay. A body
310    ///   too large to buffer cannot be re-sent, so the response is forwarded
311    ///   downstream unchanged and a warning is logged.
312    ///
313    /// **The implementation is responsible for bounding retries itself.** This
314    /// is called on every attempt, so an implementation that always returns
315    /// `true` will retry until the server-wide limit is reached and then
316    /// surface a proxy error rather than the upstream response. Returning
317    /// `false` on the final attempt is what lets the last response through,
318    /// which is almost always what an operator wants — three failed tries
319    /// should end with the upstream's 503, not a generic gateway error.
320    ///
321    /// Responses served from cache never trigger this.
322    fn should_retry_response(
323        &self,
324        _session: &Session,
325        _resp: &ResponseHeader,
326        _ctx: &mut Self::CTX,
327    ) -> bool
328    where
329        Self::CTX: Send + Sync,
330    {
331        false
332    }
333
334    /// The modification is before caching, so any change here will be stored in the cache if enabled.
335    ///
336    /// Responses served from cache won't trigger this filter. If the cache needed revalidation,
337    /// only the 304 from upstream will trigger the filter (though it will be merged into the
338    /// cached header, not served directly to downstream).
339    async fn upstream_response_filter(
340        &self,
341        _session: &mut Session,
342        _upstream_response: &mut ResponseHeader,
343        _ctx: &mut Self::CTX,
344    ) -> Result<()>
345    where
346        Self::CTX: Send + Sync,
347    {
348        Ok(())
349    }
350
351    /// Modify the response header before it is send to the downstream
352    ///
353    /// The modification is after caching. This filter is called for all responses including
354    /// responses served from cache.
355    async fn response_filter(
356        &self,
357        _session: &mut Session,
358        _upstream_response: &mut ResponseHeader,
359        _ctx: &mut Self::CTX,
360    ) -> Result<()>
361    where
362        Self::CTX: Send + Sync,
363    {
364        Ok(())
365    }
366
367    // custom_forwarding is called when downstream and upstream connections are successfully established.
368    #[doc(hidden)]
369    async fn custom_forwarding(
370        &self,
371        _session: &mut Session,
372        _ctx: &mut Self::CTX,
373        _custom_message_to_upstream: Option<mpsc::Sender<Bytes>>,
374        _custom_message_to_downstream: mpsc::Sender<Bytes>,
375    ) -> Result<()>
376    where
377        Self::CTX: Send + Sync,
378    {
379        Ok(())
380    }
381
382    // received a custom message from the downstream before sending it to the upstream.
383    #[doc(hidden)]
384    async fn downstream_custom_message_proxy_filter(
385        &self,
386        _session: &mut Session,
387        custom_message: Bytes,
388        _ctx: &mut Self::CTX,
389        _final_hop: bool,
390    ) -> Result<Option<Bytes>>
391    where
392        Self::CTX: Send + Sync,
393    {
394        Ok(Some(custom_message))
395    }
396
397    // received a custom message from the upstream before sending it to the downstream.
398    #[doc(hidden)]
399    async fn upstream_custom_message_proxy_filter(
400        &self,
401        _session: &mut Session,
402        custom_message: Bytes,
403        _ctx: &mut Self::CTX,
404        _final_hop: bool,
405    ) -> Result<Option<Bytes>>
406    where
407        Self::CTX: Send + Sync,
408    {
409        Ok(Some(custom_message))
410    }
411
412    /// Similar to [Self::upstream_response_filter()] but for response body
413    ///
414    /// This function will be called every time a piece of response body is received. The `body` is
415    /// **not the entire response body**.
416    fn upstream_response_body_filter(
417        &self,
418        _session: &mut Session,
419        _body: &mut Option<Bytes>,
420        _end_of_stream: bool,
421        _ctx: &mut Self::CTX,
422    ) -> Result<Option<Duration>> {
423        Ok(None)
424    }
425
426    /// Similar to [Self::upstream_response_filter()] but for response trailers
427    fn upstream_response_trailer_filter(
428        &self,
429        _session: &mut Session,
430        _upstream_trailers: &mut header::HeaderMap,
431        _ctx: &mut Self::CTX,
432    ) -> Result<()> {
433        Ok(())
434    }
435
436    /// Similar to [Self::response_filter()] but for response body chunks
437    fn response_body_filter(
438        &self,
439        _session: &mut Session,
440        _body: &mut Option<Bytes>,
441        _end_of_stream: bool,
442        _ctx: &mut Self::CTX,
443    ) -> Result<Option<Duration>>
444    where
445        Self::CTX: Send + Sync,
446    {
447        Ok(None)
448    }
449
450    /// Similar to [Self::response_filter()] but for response trailers.
451    /// Note, returning an Ok(Some(Bytes)) will result in the downstream response
452    /// trailers being written to the response body.
453    ///
454    /// TODO: make this interface more intuitive
455    async fn response_trailer_filter(
456        &self,
457        _session: &mut Session,
458        _upstream_trailers: &mut header::HeaderMap,
459        _ctx: &mut Self::CTX,
460    ) -> Result<Option<Bytes>>
461    where
462        Self::CTX: Send + Sync,
463    {
464        Ok(None)
465    }
466
467    /// This filter is called when the entire response is sent to the downstream successfully or
468    /// there is a fatal error that terminate the request.
469    ///
470    /// An error log is already emitted if there is any error. This phase is used for collecting
471    /// metrics and sending access logs.
472    async fn logging(&self, _session: &mut Session, _e: Option<&Error>, _ctx: &mut Self::CTX)
473    where
474        Self::CTX: Send + Sync,
475    {
476    }
477
478    /// A value of true means that the log message will be suppressed. The default value is false.
479    fn suppress_error_log(&self, _session: &Session, _ctx: &Self::CTX, _error: &Error) -> bool {
480        false
481    }
482
483    /// This filter is called when there is an error **after** a connection is established (or reused)
484    /// to the upstream.
485    fn error_while_proxy(
486        &self,
487        peer: &HttpPeer,
488        session: &mut Session,
489        e: Box<Error>,
490        _ctx: &mut Self::CTX,
491        client_reused: bool,
492    ) -> Box<Error> {
493        let mut e = e.more_context(format!("Peer: {}", peer));
494        // only reused client connections where retry buffer is not truncated
495        e.retry
496            .decide_reuse(client_reused && !session.as_ref().retry_buffer_truncated());
497        e
498    }
499
500    /// This filter is called when there is an error in the process of establishing a connection
501    /// to the upstream.
502    ///
503    /// In this filter the user can decide whether the error is retry-able by marking the error `e`.
504    ///
505    /// If the error can be retried, [Self::upstream_peer()] will be called again so that the user
506    /// can decide whether to send the request to the same upstream or another upstream that is possibly
507    /// available.
508    fn fail_to_connect(
509        &self,
510        _session: &mut Session,
511        _peer: &HttpPeer,
512        _ctx: &mut Self::CTX,
513        e: Box<Error>,
514    ) -> Box<Error> {
515        e
516    }
517
518    /// This filter is called when the request encounters a fatal error.
519    ///
520    /// Users may write an error response to the downstream if the downstream is still writable.
521    ///
522    /// The response status code of the error response may be returned for logging purposes.
523    /// Additionally, the user can return whether this session may be reused in spite of the error.
524    /// Today this reuse status is only respected for errors that occur prior to upstream peer
525    /// selection, and the keepalive configured on the `Session` itself still takes precedent.
526    async fn fail_to_proxy(
527        &self,
528        session: &mut Session,
529        e: &Error,
530        _ctx: &mut Self::CTX,
531    ) -> FailToProxy
532    where
533        Self::CTX: Send + Sync,
534    {
535        let code = match e.etype() {
536            HTTPStatus(code) => *code,
537            _ => {
538                match e.esource() {
539                    ErrorSource::Upstream => 502,
540                    ErrorSource::Downstream => {
541                        match e.etype() {
542                            WriteError | ReadError | ConnectionClosed => {
543                                /* conn already dead */
544                                0
545                            }
546                            _ => 400,
547                        }
548                    }
549                    ErrorSource::Internal | ErrorSource::Unset => 500,
550                }
551            }
552        };
553        if code > 0 {
554            session.respond_error(code).await.unwrap_or_else(|e| {
555                error!("failed to send error response to downstream: {e}");
556            });
557        }
558
559        FailToProxy {
560            error_code: code,
561            // default to no reuse, which is safest
562            can_reuse_downstream: false,
563        }
564    }
565
566    /// Decide whether should serve stale when encountering an error or during revalidation
567    ///
568    /// An implementation should follow
569    /// <https://datatracker.ietf.org/doc/html/rfc9111#section-4.2.4>
570    /// <https://www.rfc-editor.org/rfc/rfc5861#section-4>
571    ///
572    /// This filter is only called if cache is enabled.
573    // 5xx HTTP status will be encoded as ErrorType::HTTPStatus(code)
574    fn should_serve_stale(
575        &self,
576        _session: &mut Session,
577        _ctx: &mut Self::CTX,
578        error: Option<&Error>, // None when it is called during stale while revalidate
579    ) -> bool {
580        // A cache MUST NOT generate a stale response unless
581        // it is disconnected
582        // or doing so is explicitly permitted by the client or origin server
583        // (e.g. headers or an out-of-band contract)
584        error.is_some_and(|e| e.esource() == &ErrorSource::Upstream)
585    }
586
587    /// This filter is called when the request just established or reused a connection to the upstream
588    ///
589    /// This filter allows user to log timing and connection related info.
590    async fn connected_to_upstream(
591        &self,
592        _session: &mut Session,
593        _reused: bool,
594        _peer: &HttpPeer,
595        #[cfg(unix)] _fd: std::os::unix::io::RawFd,
596        #[cfg(windows)] _sock: std::os::windows::io::RawSocket,
597        _digest: Option<&Digest>,
598        _ctx: &mut Self::CTX,
599    ) -> Result<()>
600    where
601        Self::CTX: Send + Sync,
602    {
603        Ok(())
604    }
605
606    /// This callback is invoked every time request related error log needs to be generated
607    ///
608    /// Users can define what is important to be written about this request via the returned string.
609    fn request_summary(&self, session: &Session, _ctx: &Self::CTX) -> String {
610        session.as_ref().request_summary()
611    }
612
613    /// Whether the request should be used to invalidate(delete) the HTTP cache
614    ///
615    /// - `true`: this request will be used to invalidate the cache.
616    /// - `false`: this request is a treated as a normal request
617    fn is_purge(&self, _session: &Session, _ctx: &Self::CTX) -> bool {
618        false
619    }
620
621    /// This filter is called after the proxy cache generates the downstream response to the purge
622    /// request (to invalidate or delete from the HTTP cache), based on the purge status, which
623    /// indicates whether the request succeeded or failed.
624    ///
625    /// The filter allows the user to modify or replace the generated downstream response.
626    /// If the filter returns `Err`, the proxy will instead send a 500 response.
627    fn purge_response_filter(
628        &self,
629        _session: &Session,
630        _ctx: &mut Self::CTX,
631        _purge_status: PurgeStatus,
632        _purge_response: &mut std::borrow::Cow<'static, ResponseHeader>,
633    ) -> Result<()> {
634        Ok(())
635    }
636}
637
638/// Context struct returned by `fail_to_proxy`.
639pub struct FailToProxy {
640    pub error_code: u16,
641    pub can_reuse_downstream: bool,
642}