A proxy framework where routing, load balancing, and security are all the same primitive - composed into pipelines, swapped at runtime, without dropping a request.
Most proxies bolt features on as special cases. Praxis takes a different approach: every behavior is a filter, and built-in filters use the exact same traits as user-written ones.
Cluster routing, load balancing, rate limiting, body inspection, TLS policy - all filters. They implement HttpFilter or TcpFilter, get composed into pipelines, and execute in declared order. That single decision makes Praxis a framework for building proxies, not just a proxy you configure.
Each filter returns a FilterAction telling the pipeline what to do next: Continue to the next filter, Reject to stop with an error, or TerminalResponse to return a complete response while still running response-phase filters. No special cases.
Praxis does not write its own event loop, TLS stack, or HTTP parser. It builds on Pingora - a battle-tested Rust proxy framework handling trillions of requests per day in production.
Connection handling, HTTP/1 and HTTP/2 framing, connection pooling, request smuggling prevention, the async runtime - all inherited. Praxis spends its code budget on the layer above: the filter model, configuration, and composition engine.
Pingora is quarantined behind adapters. Only the protocol and core crates import it directly. The filter model and everything you build on top never touch Pingora types. That keeps the dependency contained and the framework surface clean.
Praxis is a Cargo workspace of focused crates, not one monolith. Each crate has a single job and a clear boundary. Capabilities sit side by side and depend on each other explicitly.
The pipeline engine, HttpFilter and TcpFilter traits, condition evaluation, branch chains, load balancing strategies, and every built-in filter. The heart of the composition model.
Configuration types (YAML via serde), validation, error types, server factory, subrequest client, circuit breaker, and memory pressure monitoring.
Protocol adapters translating Pingora callbacks into pipeline invocations. The only crate (with core) that imports Pingora directly.
TLS configuration, certificate loading, SNI-based cert selection, hot-reloadable cert resolver, mTLS client auth, and CRL support.
On top sits praxis-proxy - the proxy binary, kept thin because the real capability lives in the framework crates below. Alongside them, a full test band: conformance, integration, resilience, security, and schema - each its own workspace member.
The dependency direction is strict and one-way: binary depends on framework crates, they depend on adapters, adapters depend on Pingora. Nothing points back up.
A request enters and passes through filters in declared order. Each can read, rewrite, or short-circuit. On the way back, response filters run in reverse order - so the filter that opened a concern is the one that closes it.
Along the way, filters populate a shared HttpFilterContext: the router sets the target cluster, the load balancer picks the upstream. Filters communicate through durable metadata (key-value pairs persisting the entire request lifetime), transient filter results (for branch conditions), and header mutations.
Filters also declare how they access the body. Three modes exist: Stream (pass-through), StreamBuffer (buffer up to N bytes before deciding), and SizeLimit (enforce a maximum). Multiple filters requesting different modes produces the most capable mode - modes only ratchet upward, never down.
Filters are grouped into named chains - a "security" chain, an "observability" chain, a "traffic" chain. Each listener references the chains it wants, and at startup they are concatenated into one flat pipeline.
Startup-time: which chains a listener concatenates into its pipeline. Structural and validated before any request arrives.
Request-time: the router filter picks an upstream cluster by path, host, and headers. Dynamic and per-request.
Chain boundaries disappear at runtime - the pipeline is a flat Vec<PipelineFilter> with pre-computed body capabilities. Validation catches problems at startup: load balancers without routers, unconditional static responses, security filters bypassed by branch chains.
Praxis replaces filter pipelines at runtime without restarting or dropping in-flight requests. Each listener holds an ArcSwap<FilterPipeline>. Every request loads a snapshot pinned for its lifetime; a reload atomically stores a new pipeline.
A file watcher monitors the config directory with 500ms debounce and content hashing to skip unchanged files. If validation fails, nothing changes. The new config is fully parsed, all pipelines rebuilt, and only then swapped in atomically. Exponential backoff (1s base, 60s max) handles transient errors.
What reloads dynamically: filter chains, cluster definitions, body limits, KV stores. What requires a restart: listener addresses, protocol type, TLS toggles. The distinction is detected by diffing and logged, never half-applied.
Praxis ships with a broad set of built-in filters covering security, traffic management, transformation, and observability. All use the same traits a custom filter would.
cors, csrf, ip_acl, credential_injection, forwarded_headers, guardrails, peer_identity_trust, policy
router, load_balancer, rate_limit, circuit_breaker, timeout, redirect, static_response
headers, path_rewrite, url_rewrite, compression (gzip, brotli, zstd)
access_log, request_id, json_body_field, json_rpc, grpc_detection
Four load balancing strategies are built in: weighted round-robin, least connections (atomic in-flight counters), consistent hashing (stable endpoint by header or path), and power-of-two-choices (sample two, pick the less loaded). All are health-aware and skip endpoints marked unhealthy by active health checks.
TCP filters cover sni_router (route by TLS SNI), tcp_load_balancer, and tcp_access_log. An AnyFilter wrapper lets HTTP and TCP filters coexist in one pipeline. HTTP listeners accept both; TCP listeners accept only TCP filters. Mismatches are caught at startup.