One of Pingora’s core strengths is its programmable request lifecycle. At almost every stage — before connecting upstream, after receiving a response, before sending to the client — you can inspect and rewrite headers, redirect traffic, or abandon the proxy flow entirely to write a custom response. This guide walks through the most common patterns: routing by path, rewriting headers, short-circuiting with error pages, and transforming response bodies.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/pingora/llms.txt
Use this file to discover all available pages before exploring further.
Routing by Path
Theupstream_peer() phase has full access to the request via session.req_header(). You can inspect any attribute of the request — method, path, query string, headers — to decide which upstream to route to.
The example below routes requests whose path starts with /family/ to 1.0.0.1 and all other requests to 1.1.1.1:
upstream_peer() arrives at its result.
Modifying Request Headers
Useupstream_request_filter() to add, modify, or remove the headers that are sent to the upstream server. This filter receives a mutable RequestHeader and fires after the automatic hop-by-hop stripping policy has been applied, so headers you add here are treated as application-controlled and will be forwarded as-is.
Modifying Response Headers
Useresponse_filter() to rewrite the response headers before they reach the downstream client. This filter is called for all responses — including those served from cache — so it is the right place for headers that must always be present or absent in the final response.
The example below replaces the Server header with a custom value and removes the alt-svc header (useful if your proxy doesn’t support HTTP/3):
upstream_response_filter() instead.
Returning Custom Responses
Sometimes you want to reject or redirect a request entirely rather than proxying it. ReturnOk(true) from request_filter() to tell Pingora that you have already written a response and the proxy flow should stop. Returning Ok(false) continues to the next phase as normal.
The example below checks for an Authorization header on /login paths and returns a 403 response if the credentials are missing:
session.respond_error(code) is a convenience method that writes a minimal HTTP error response with the given status code. For full control — including custom headers and a custom body — use session.write_response_header() and session.write_response_body() directly.
Modifying Response Bodies
response_body_filter() is called once for each chunk of the response body as it is streamed to the downstream. The body argument is a mutable Option<Bytes> — set it to None to suppress a chunk, replace it with new Bytes to rewrite it, or leave it as-is to pass it through.
The end_of_stream flag is true on the final call, allowing you to append a trailer or finalize any transformation.
Duration return value introduces a deliberate delay before the next body chunk is sent, which can be used to throttle download speed.
Adding Access Logging
Thelogging() phase runs at the very end of every request — success or failure — and is the natural place for access logging and metrics. It receives the final error (if any) via Option<&Error>, and you can read the response status code from session.response_written().
