Open-Source Wikis

/

Pingora

/

How to contribute

/

Patterns and conventions

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 uses async-trait = "0.1" (workspace dependency) for trait methods that need to be async.
  • Tokio's runtime is wrapped by pingora-runtime so callers don't pick a flavor at the call site. Spawn with tokio::spawn inside services; spawn long-running background work with pingora-core::services::background_service.
  • Don't block_on inside service code. Pingora has a blocking-pool config (tokio.blocking_threads) for genuine CPU-blocking work; use tokio::task::spawn_blocking for that.
  • Use pingora_timeout for timeouts — tokio::time works but pingora_timeout::fast_timeout::fast_sleep is 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 into proxy_cache.rs and proxy_purge.rs.
  • mod.rs files are kept small — they re-export, declare submodules, and define a few top-level types. Logic moves into siblings.
  • lib.rs for a crate often does double duty as the main module file. pingora-cache/src/lib.rs is ~1,800 lines because the HttpCache state 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 Ctx or ProxyCtx (lowercase second letter is intentional).

Visibility

  • pub is reserved for the public API surface. Search a crate for pub fn to 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 internals
  • log::debug! for normal flow markers
  • log::info! sparingly — startup, graceful upgrade, lifecycle changes
  • log::warn! for unexpected-but-recoverable
  • log::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/*.rs for integration tests. Reuse tests/utils/ for mock servers.
  • Don't share global state between tests (use 127.0.0.1:0 to bind ephemeral ports).
  • For HTTP/2 tests, the workspace pins h2 = ">=0.4.11" and uses tokio_test.

Documentation comments

  • Every pub item should have a /// doc comment.
  • Use # Examples sections 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.

Patterns and conventions – Pingora wiki | Factory