Filter Chain
The filter chain is Meridian’s extension point. Filters are async functions that inspect and modify HTTP requests and responses as they flow through the proxy.
Design
Filters are composed in a chain. Requests flow forward through the chain; responses flow backward:
Request: Client → Filter 1 → Filter 2 → Filter 3 → Router → Upstream
Response: Client ← Filter 1 ← Filter 2 ← Filter 3 ← Router ← Upstream
The first filter to see the request is the last to see the response.
The HttpFilter Trait
#![allow(unused)]
fn main() {
#[async_trait]
pub trait HttpFilter: Send + Sync + 'static {
async fn on_request(&self, req: &mut RequestContext)
-> Result<RequestAction, FilterError>;
async fn on_response(&self, resp: &mut ResponseContext)
-> Result<ResponseAction, FilterError>;
fn on_complete(&self, ctx: &ExchangeContext) {}
fn name(&self) -> &'static str;
}
}
Filters are async fn — not callbacks. A filter that needs to make an external call (auth service, rate limit check) simply awaits it. No manual state machines.
Request Actions
A request filter returns one of:
| Action | Effect |
|---|---|
Continue | Pass to the next filter |
SendResponse(response) | Short-circuit: send this response immediately, skip remaining filters and upstream |
Redirect { location, status } | Redirect the client |
Response Actions
A response filter returns one of:
| Action | Effect |
|---|---|
Continue | Pass to the next filter (toward client) |
Replace(response) | Replace the entire response |
Inter-Filter Communication
Filters communicate via typed metadata attached to the request/response context:
#![allow(unused)]
fn main() {
// Filter A stores a decision
ctx.metadata.insert(AuthDecision { allowed: true, user: "alice" });
// Filter B reads it
if let Some(auth) = ctx.metadata.get::<AuthDecision>() {
// ...
}
}
Metadata uses TypeId-keyed storage — O(1) lookup, type-safe, collision-free. Internally backed by a Vec with linear scan (benchmarked at 1.6ns per lookup for typical 1-5 entries).
Dynamic Filter Chain
DynamicFilterChain holds a Vec<Arc<dyn HttpFilter>> for runtime-configurable chains:
#![allow(unused)]
fn main() {
let chain = DynamicFilterChain::from_filters(vec![
Arc::new(AuthFilter::new()),
Arc::new(RateLimitFilter::new()),
Arc::new(LoggingFilter::new()),
]);
// Request path: runs filters in order
let action = chain.execute_request(&mut req_ctx).await?;
// Response path: runs filters in REVERSE order
let action = chain.execute_response(&mut resp_ctx).await?;
}
Performance
| Operation | Measured |
|---|---|
| 5-filter chain dispatch (noop) | 19ns |
| Metadata lookup (hit) | 1.6ns |
| Metadata insert | 21ns |
| Short-circuit (reject at filter 1/5) | 13ns |