Middleware¶
Some logic doesn't belong in any one handler — it belongs around all of them: authentication, sessions, CSRF protection, rate limiting, request logging. Middleware is where that lives. Each one wraps the request, runs code on the way in and on the way out, and decides whether to pass control deeper or stop short.
Needs the [http] extra
Middleware runs in the serve path — uv add 'arvel[http]' (Litestar).
Defining middleware¶
Subclass Middleware and implement handle(request, call_next). Call call_next to continue the
pipeline; return a Response instead to short-circuit:
from arvel.http.middleware import Middleware
class MeasureTime(Middleware):
async def handle(self, request, call_next):
start = time.perf_counter()
response = await call_next(request) # run the rest of the pipeline + handler
log.info("request", path=request.path(), ms=(time.perf_counter() - start) * 1000)
return response
The pattern is an onion: code before call_next runs on the way in, code after runs on the way
out, and short-circuiting (returning without calling call_next) skips everything inward.
Registering middleware¶
Per request the middleware run outermost-first in three tiers: global → group → route.
Global middleware¶
To run a middleware on every HTTP request, list it in bootstrap/middlewares.py (the list the
fluent with_middlewares(...) loads) — it runs after the built-in global gate:
# bootstrap/middlewares.py
from arvel.http.middleware import AuthenticateMiddleware
middlewares = [AuthenticateMiddleware] # resolves the request user into `current_user`
Assigning middleware to routes¶
Attach a group, or specific middleware, when you define routes — a single route directly, or a whole group block:
Route.get("/dashboard", show, group="web") # run the web group (session + CSRF)
with router.group(group="web", middleware=[Authenticate]):
router.get("/account", account) # web group + Authenticate
router.get("/billing", billing).middleware(EnsureSubscribed) # + one more, this route only
Middleware groups¶
Routes belong to a middleware group. arvel ships two:
| Group | Default middleware | For |
|---|---|---|
web |
StartSession, ShareErrorsFromSession, ValidateCsrfToken |
browser pages (cookies, sessions, CSRF, flashed errors) |
api |
ThrottleRequests |
stateless JSON APIs (rate-limited) |
Populate the defaults at boot and tweak them, or define your own named group:
kernel.use_default_groups() # web + api with their defaults
kernel.append_to_group("web", EnsureTeamSelected) # add to a group
kernel.prepend_to_group("api", TrustProxies) # run first
kernel.middleware_group("admin", [Authenticate, EnsureAdmin]) # a new named group
Sorting middleware¶
Insertion order (global → group → route) is usually enough, but sometimes two middleware need a
fixed relative order regardless of which tier put them on the stack — e.g. a session must start
before anything that reads it. Set kernel.middleware_priority to a list of middleware classes
in the order they must run:
The full stack is stably re-sorted: any middleware named in the list runs in that relative order no matter where it was inserted; everything else keeps its position. Leave it empty for plain insertion order.
Middleware parameters¶
Middleware can receive parameters. Register a short alias for a class, then reference it by name — and, for parameterized middleware, pass an argument after a colon:
The throttle:<name> form is the built-in example — it selects a named rate limiter. See
Rate Limiting for defining limiters.
Terminable middleware¶
Sometimes middleware needs to do work once the response is built — writing a session, flushing a log.
Implement terminate(request, response) in addition to handle; the kernel runs it after the response
is built but before it's returned to the ASGI server, on the same instance (so state set in
handle is visible in terminate). Because it runs before delivery it adds to request latency — keep
it light and push anything slow onto the queue:
class StoreVisit(Middleware):
async def handle(self, request, call_next):
self.started = time.time()
return await call_next(request)
async def terminate(self, request, response):
await record_visit(request.path(), took=time.time() - self.started)
Available middleware¶
Global middleware run on every request and are on by default — you don't wire them up:
PreventRequestsDuringMaintenance—503while the app is in maintenance mode.ValidatePostSize—413on a body larger thanconfig('app.max_request_size')(default 10 MiB).ValidateHost—400when theHostisn't inconfig('app.trusted_hosts')(a no-op until configured).TrimStrings— strips leading/trailing whitespace from every string in the parsed input;password/password_confirmation/current_passwordare left untouched.ConvertEmptyStringsToNull— turns every""intoNone, so anullablefield submitted empty passes and arequiredone fails.MethodOverride— HTML form method spoofing.
Group / opt-in middleware:
EncryptCookies— encrypts every outgoing cookie with the app key (active whenAPP_KEYis set; theXSRF-TOKENcookie is excepted by design).StartSession— attaches therequest.sessiondict from the session store (see Session).ShareErrorsFromSession— exposes flashed validationerrorsto views (see Validation).ValidateCsrfToken— the web group's CSRF guard (see CSRF Protection).ThrottleRequests— the rate limiter (see Rate Limiting).RequestContext,Locale,Authenticate— bind a request id, resolve the request locale, and resolve the current user.Localepicks the locale by precedence: an explicit switch (?lang=/ alocalecookie), then the signed-in user's saved preference, thenAccept-Language(see Localization).
Common mistakes & gotchas¶
- Forgetting to return the response.
handlemust return whatcall_nextgave you (or your ownResponse). ReturningNonedrops the response. - Heavy work on every request. Middleware runs for every matching request — keep it lean; push slow work to the queue.
- CSRF on an API. The
apigroup is stateless — don't addValidateCsrfTokenthere; use a token guard for API auth instead.
See also¶
- Routing — assigning routes to groups.
- CSRF Protection — the
webgroup's CSRF token flow, SPA cookies, exemptions. - Rate Limiting — named limiters and
throttle:<name>. - Authentication — the auth middleware + guards.