Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ActionEffect
ContinuePass 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:

ActionEffect
ContinuePass 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

OperationMeasured
5-filter chain dispatch (noop)19ns
Metadata lookup (hit)1.6ns
Metadata insert21ns
Short-circuit (reject at filter 1/5)13ns