New project ("wanaku-praxis") that builds on the praxis proxy framework to add custom protocol handling, starting with MCP→gRPC transformation and eventually a management API.
Why: Praxis is designed as a framework — praxis-ai demonstrates the pattern. A new project follows the same model: depend on praxis core crates, implement domain-specific filters, register them, and call run_server_with_registry().
How to apply: Use this file as the bootstrap reference when creating the project.
Praxis Framework Overview
Praxis is a general-purpose high-performance reverse proxy framework built on Pingora. It provides:
HttpFiltertrait andFilterPipelineengine for request/response processing- Built-in filters: routing, load balancing, rate limiting, IP ACLs, CORS, compression, circuit breaking, header manipulation, JSON-RPC parsing, gRPC detection, etc.
- Protocol layer:
PingoraHttpandPingoraTcpadapters wrapping Pingora - Hot-reload config via
ArcSwap— atomic pipeline replacement without restart FilterRegistrywithregister_filters!/export_filters!macros for extensibility- Admin endpoints:
/healthy,/ready,/metrics(Prometheus),/api/kv/*(CRUD) - Health check runner (HTTP, HTTP/2, TCP probes per cluster)
- Published as
praxis-proxy-*crates:praxis-proxy-core,praxis-proxy-filter,praxis-proxy-protocol,praxis-proxy-tls
Project Structure (follow praxis-ai's pattern)
wanaku-praxis/
├── Cargo.toml # workspace root
├── server/ # binary crate (wanaku-praxis binary)
│ └── src/
│ ├── main.rs # entry point, CLI
│ ├── lib.rs # register_filters! macro, build_full_registry()
│ ├── server.rs # calls run_server_with_registry()
│ └── pipelines.rs # resolve_pipelines() with custom PipelineExtensions
├── filters/ # filter implementations
│ └── src/
│ └── ... # one module per filter
├── apis/ # protocol types, parsers, clients
│ └── src/
│ └── ...
└── tests/
└── integration/ # integration testsDependencies on Praxis Core
[dependencies]
praxis-core = { package = "praxis-proxy-core", version = "0.4" }
praxis-filter = { package = "praxis-proxy-filter", version = "0.4" }
praxis-protocol = { package = "praxis-proxy-protocol", version = "0.4" }
praxis-tls = { package = "praxis-proxy-tls", version = "0.4" }Or path deps during development:
praxis-core = { package = "praxis-proxy-core", path = "../praxis/core" }
praxis-filter = { package = "praxis-proxy-filter", path = "../praxis/filter" }
praxis-protocol = { package = "praxis-proxy-protocol", path = "../praxis/protocol" }
praxis-tls = { package = "praxis-proxy-tls", path = "../praxis/tls" }Key Extension Points
1. Custom Filter Registration
use praxis_filter::register_filters;
register_filters! {
http "my_filter" => MyFilter::from_config,
}Generates a custom_registry() function that starts with builtins and adds user filters. Pass to run_server_with_registry().
2. HttpFilter Trait
Every filter implements:
name()→ filter name stringrequest_body_access()/response_body_access()→BodyAccess::None | ReadOnly | ReadWriterequest_body_mode()/response_body_mode()→BodyMode::Stream | StreamBuffer { max_bytes } | SizeLimit { max_bytes }on_request()/on_request_body()/on_response()/on_response_body()→FilterAction- Constructor:
from_config(&serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError>
3. PipelineExtension Trait
Inject per-request resources that filters retrieve from RequestExtensions:
impl PipelineExtension for MyRegistry {
fn prepare(&self, extensions: &mut RequestExtensions) {
extensions.insert(self.clone());
}
}Register via FilterPipeline::add_pipeline_extension().
4. Body Rewriting Pattern (from anthropic_to_openai precedent)
- Request:
BodyAccess::ReadWrite+BodyMode::StreamBuffer { max_bytes }→ full body arrives in singleon_request_body()call at EOS → parse → transform →*body = Some(Bytes::from(new_bytes)) - Response: start as
BodyMode::Stream, dynamically upgrade toStreamBufferinon_response()for non-streaming → transform at EOS - Streaming responses: process per-chunk in
on_response_body()withBodyMode::Stream - Header mutation:
ctx.extra_request_headers(request),ctx.response_header.headers(response)
5. Inter-filter Communication
ctx.set_metadata(key, value)— flat string KV (64-byte key, 256-byte value limits)ctx.set_structured_metadata(namespace, key, value)— nested JSON per namespacectx.filter_results— classifier → router communicationctx.insert_filter_state::<T>()/ctx.get_filter_state::<T>()— typed per-filter state
6. Synthetic Responses
Filters can short-circuit with locally-generated responses:
FilterAction::Reject(Rejection::status(200).with_body(json_bytes).with_header("Content-Type", "application/json"))Classify → Route → Branch Pattern
Classifier filters inspect requests, promote facts to filter results. The core router matches those results to select backend clusters. Downstream filters branch on the classification.
Example flow: my_classifier (writes format=grpc to filter_results) → router (matches format=grpc → grpc-cluster) → load_balancer → backend.
Future Goals
MCP→gRPC Transformation (see [[mcp-to-grpc-transformation]])
- Body rewriting: JSON-RPC → protobuf + gRPC framing (request), reverse on response
- Blocker: praxis core has no upstream HTTP/2 selection API. Needs cluster-level
upstream_http_version: h2or filter-levelctx.set_upstream_http_version(). Pingora supports it; praxis doesn't expose it. - Needs
prostdependency and.protodefinitions for target gRPC service tonic/tonic-prostalready in praxis core workspace deps (test-only currently)
Admin Management API (see [[admin-management-api]])
- Register a separate Pingora service on a management port
- Needs
Archandles toListenerPipelines,Config,FilterRegistry,KvStoreRegistry - Config mutation: either write YAML to disk (file watcher picks up) or call
resolve_pipelines()+swap()directly - No praxis core changes needed initially;
AdminExtensiontrait could be upstreamed later
Config System
YAML config maps to Config struct. Key sections:
listeners— bind addresses, protocol, TLS, filter chain referencesfilter_chains— named ordered lists of filter entriesclusters— backend endpoint groups with health checksadmin— admin port config (loopback-only by default)metrics— Prometheus configruntime— connection/memory limits
Conventions (inherited from praxis)
- Rust stable 1.96+, edition 2024
#![deny(unsafe_code)]in all crate roots#[expect(...)]withreasoninstead of#[allow(...)]- Clippy with
-D warnings - Errors via
thiserror, logging viatracing - New features require: unit tests, integration tests, example config, functional integration test
- Body rewriting: prefer streaming over buffering; keep buffer limits explicit
- No holding locks across
.awaitpoints