---
title: "Native HTTP filters"
url: "/docs/extend/http-filters-in-rust/"
kind: "concept"
summary: "How a native filter is wired into the Arion binary: the config enum, the type URL registry, the runtime dispatch, and per-route overrides."
---

> Arion site index: [/llms.txt](/llms.txt)

# Native HTTP filters

How a native filter is wired into the Arion binary: the config enum, the type URL registry, the runtime dispatch, and per-route overrides.

A native HTTP filter is Rust code compiled into Arion itself — the same mechanism behind the built-in RBAC, JWT, CORS, and MCP gateway filters. This page maps the seams a contributor touches to add one; it teaches the shape, not a full tutorial.

## Why it matters

Native filters beat [WASM](@/docs/extend/wasm-filters.md) where the feature belongs to everyone: always-on behaviour, zero serialization across a guest boundary, direct access to internals such as metrics, access logging, and streaming bodies. The price is that shipping one means changing Arion — a contribution and a rebuild, not a config change.

## The config side

A filter's configuration lives in `arion-configuration`, in `src/config/network_filters/http_connection_manager/http_filters/`. Each filter is one module with a serde config struct, plus a variant on the `HttpFilterType` enum:

```rust
#[serde(rename_all = "snake_case", tag = "filter_type", content = "filter_settings")]
pub enum HttpFilterType {
    Wasm(WasmConfig),
    Rbac(HttpRbac),
    RateLimit(LocalRateLimit),
    ExternalProcessor(ExternalProcessor),
    // ... Cors, JwtAuthentication, McpGateway, UserRateLimit, CedarPolicy
}
```

The Envoy dialect reaches this enum through the `envoy_conversions` module in `http_filters.rs`, where `TryFrom<Any> for SupportedEnvoyFilter` maps a `@type` URL to a protobuf decoder. Arion-original filters register URLs under the `arion.` namespace:

```rust
"type.googleapis.com/arion.extensions.filters.http.cedar.cedar_policy.v3.CedarPolicy" => {
    ProtoCedarPolicy::decode(typed_config.value.as_slice()).map(Self::CedarPolicy)
},
```

Each config struct implements `TryFrom` for its Envoy proto, destructuring every field and passing unhandled ones through `unsupported_field!`. That macro is the [compatibility promise](@/docs/concepts/envoy-compatibility.md) in code: a field the filter does not implement makes the config refuse to load rather than being silently dropped.

## The runtime side

The executable filter lives in `arion-lib`, under `src/listeners/http_connection_manager/`, and gets a variant on `HttpFilterValue` in `src/listeners/http_filters.rs`. `TryFrom<HttpFilterConfig>` builds the runtime filter from its config once at load; per request, `FilterFactory::new_from` produces a request-local instance. The chain then dispatches:

```rust
pub enum FilterDecision {
    Continue,
    Reroute,
    DirectResponse(Box<Response<ArionResponseBody>>),
}

impl HttpFilterValue {
    pub async fn apply_request(&mut self, request: &mut Request<ArionRequestBody>, ctx: &RequestCtx) -> FilterDecision {
        match self {
            HttpFilterValue::Rbac(rbac) => apply_authorization_rules(rbac, request),
            HttpFilterValue::ExternalProcessor(ext_proc) => ext_proc.apply_request(request, ctx).await,
            // ...
        }
    }
}
```

A filter implements `apply_request`, and `apply_response` if it acts on the response path. `Continue` passes the message on, `DirectResponse` answers the client and stops the chain, and `Reroute` re-matches the route table and resumes with the next filter. Request filters run in configured order; response filters run in reverse. Filters that skip a path — RBAC and rate limiting have no response behaviour — return `Continue` from the unused hook.

## Per-route overrides

Route-level control flows through `typed_per_filter_config`, keyed by the filter's chain name. On the config side that is the `FilterConfigOverride` enum (today: local rate limit, RBAC, ext_proc) plus the generic `FilterConfig` wrapper for disabling any filter; at load time `per_route_http_filters` precomputes the effective filter list for every route, so request handling only does a lookup. A new filter that wants per-route settings adds a variant there and handles it in `HttpFilterValue::from_filter_override`.

## Where tests live

End-to-end coverage sits in `arion-e2e-tests/tests/` (`filters_ext_proc.rs`, `filters_wasm.rs`, `filters_rate_limit.rs`, ...), with configs composed by the builders in `arion-e2e-tests/src/config_builder/` — a new filter adds a builder there so tests can express it.

## Key terms

- **`HttpFilterType`** — the serde config enum; one variant per filter.
- **`SupportedEnvoyFilter`** — the `@type` URL registry for the Envoy dialect.
- **`HttpFilterValue`** — the runtime enum whose `apply_request` / `apply_response` run on the hot path.
- **`FilterDecision`** — what a hook returns: continue, re-route, or answer directly.

## Next

Read [the filter chain](@/docs/concepts/filter-chain.md) for how the chain is assembled, or start from [Extending Arion](@/docs/extend/overview.md) if you are still choosing an extension mechanism.
