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}