cloudflare/pingora
Patterns and conventions
How to write code that fits this repo.
Error handling
The single error type is pingora_error::Error (pingora-error/src/lib.rs). It's a categorized error: it carries an ErrorType, an ErrorSource (Internal, Upstream, Downstream), an optional Box<dyn Error> cause, an optional String context, and a retry flag.
Functions that can fail return pingora_error::Result<T> (alias for Result<T, Box<Error>>). The Box is the convention — almost every internal API returns Result<T> (boxed) rather than the bare Result<T, Error>.
The OrErr extension trait (pingora-error) gives you these patterns:
use pingora_error::{OrErr, ErrorType::*};
let value = some_io().or_err(InternalError, "couldn't read config")?;
let value = something().or_fail()?; // generic failure
let value = parse(s).or_err_with(InvalidHTTPHeader, || format!("bad: {s}"))?;When you create errors directly:
return Error::e_explain(ConnectError, "couldn't connect");
return Error::e_because(Custom("foo"), "context", source_err);error.set_retry(false) if you don't want the proxy to retry. The retry flag is consulted in fail_to_connect() and error_while_proxy().
Async style
- All async functions use
async fn. The codebase usesasync-trait = "0.1"(workspace dependency) for trait methods that need to be async. - Tokio's runtime is wrapped by
pingora-runtimeso callers don't pick a flavor at the call site. Spawn withtokio::spawninside services; spawn long-running background work withpingora-core::services::background_service. - Don't
block_oninside service code. Pingora has a blocking-pool config (tokio.blocking_threads) for genuine CPU-blocking work; usetokio::task::spawn_blockingfor that. - Use
pingora_timeoutfor timeouts —tokio::timeworks butpingora_timeout::fast_timeout::fast_sleepis cheaper for sub-second timeouts because it shares timer wheels across calls.
Trait design
The big traits use #[async_trait] and have many default-empty methods. The pattern: a single required method, many optional ones with no-op defaults. ProxyHttp (pingora-proxy/src/proxy_trait.rs) is the canonical example — upstream_peer is the only required method. ProxyHttp::CTX is an associated type that lets callers attach a typed per-request context.
When extending a public trait, prefer adding new methods with default implementations rather than making breaking changes.
File and module layout
- One concept per file. The proxy state machine is split by transport:
proxy_h1.rs,proxy_h2.rs,proxy_custom.rs. Cache behavior is split off intoproxy_cache.rsandproxy_purge.rs. mod.rsfiles are kept small — they re-export, declare submodules, and define a few top-level types. Logic moves into siblings.lib.rsfor a crate often does double duty as the main module file.pingora-cache/src/lib.rsis ~1,800 lines because theHttpCachestate machine lives there. That's a deliberate choice: the state machine fits in your head better when it's all in one file.
Naming
- Sessions are protocol-level:
ServerSession,ClientSession,HttpSession. - Peers are addressable upstreams:
HttpPeer,BasicPeer. - Connectors open outbound connections; listeners accept inbound.
- Types that hold "the user's stuff" are named
CtxorProxyCtx(lowercase second letter is intentional).
Visibility
pubis reserved for the public API surface. Search a crate forpub fnto see what users get.pub(crate)for cross-module-internal-to-the-crate.pub(super)is rare but used in cache submodules.
Logging
log::trace!for hot per-request internalslog::debug!for normal flow markerslog::info!sparingly — startup, graceful upgrade, lifecycle changeslog::warn!for unexpected-but-recoverablelog::error!only for things you'd want to alert on
Don't log secrets. The test suite spot-checks for accidental header leaks.
Conditional compilation
The crate uses heavy #[cfg(...)] for:
- TLS backend selection (
#[cfg(feature = "openssl")]etc.) - Unix vs Windows (
#[cfg(unix)],#[cfg(windows)]) - Optional features (
cache,proxy,lb,time,sentry,connection_filter,adjust_upstream_modules)
When adding a feature flag, also add it to the umbrella pingora crate's feature list and to [package.metadata.docs.rs] so docs.rs builds it.
Test patterns
- Inline
#[cfg(test)] mod tests { ... }in the file under test for unit tests. crate/tests/*.rsfor integration tests. Reusetests/utils/for mock servers.- Don't share global state between tests (use
127.0.0.1:0to bind ephemeral ports). - For HTTP/2 tests, the workspace pins
h2 = ">=0.4.11"and usestokio_test.
Documentation comments
- Every
pubitem should have a///doc comment. - Use
# Examplessections in trait-level docs when the behavior isn't obvious. #![cfg_attr(docsrs, feature(doc_cfg))]is on every crate so feature-gated items show their gating in rustdoc. Keep that pattern when adding new feature-gated modules.
Breaking changes
The workspace ships in lockstep at version 0.x and breaking changes happen across minor versions. When you remove or rename a pub API, update CHANGELOG.md (it follows Keep-a-Changelog) and call the change out as a "Breaking change" in the PR description.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.