Middleware

Status: Draft; implementation checked against the development source on 2026-09-08.

What it does

Wraps request handling in an ordered chain of cross-cutting stages — error handling, CORS, logging, sessions, auth, and well-known endpoints. Each stage is an object that always exists; you arm it through config.

When to use it

Whenever you need behaviour that applies across routes rather than inside a single handler: turning exceptions into clean responses, adding CORS headers, logging requests, and so on. Use the middleware kwarg to configure stages. On AsgiServer, errors, sessions and authentication are active by default; the last two are armed by the capability mixins.

The built-in chain

The registry ships these middleware with a priority. The table shows the registry default; AsgiServer additionally arms session and auth via its mixins. Lower priority number = more outer (runs first on the way in, last on the way out):

Name

Priority

Default

Notes

errors

100

on

maps exceptions and unmatched paths to status codes

wellknown

150

off

.well-known endpoints

logging

200

off

request logging

cors

300

off

CORS headers

session

400

off

armed automatically by SessionMixin

auth

450

off

armed automatically by AuthMixin

Only the http scope is processed by the chain.

SessionMixin and AuthMixin arm their stages simply by being composed into AsgiServer, even without session_store or auth kwargs. Explicit middleware={"session": False, "auth": False} disables them.

Setup — arming a stage

Pass the middleware kwarg. A value of True arms a stage with its defaults; a dict arms it with options; False disarms it.

from genro_asgi import AsgiServer, RoutedApplication
from genro_routes import route


class App(RoutedApplication):
    mount = ""

    @route()
    def index(self) -> dict:
        return {"ok": True}


server = AsgiServer(
    applications=[App()],
    middleware={
        "cors": True,
        "logging": True,
    },
)
server.serve(host="127.0.0.1", port=8000)

CORS options

cors accepts an options dict:

server = AsgiServer(
    applications=[App()],
    middleware={"cors": {
        "allow_origins": ["https://example.com"],
        "allow_credentials": True,
        "max_age": 600,
    }},
)

Custom middleware

Register your own middleware class in the registry, then arm it like any built-in stage:

from genro_asgi import BaseMiddleware


class StampMiddleware(BaseMiddleware):
    ...


server = AsgiServer(
    applications=[App()],
    middleware_registry={"stamp": StampMiddleware},
    middleware={"stamp": {...}},
)
  • middleware_registry={"stamp": StampMiddleware} teaches the server the new name.

  • middleware={"stamp": {...}} arms it (and passes its options).

The individual built-in middleware classes are importable from genro_asgi.middleware (e.g. from genro_asgi.middleware import CORSMiddleware), and BaseMiddleware / MiddlewareMixin from genro_asgi.

How to verify it

With cors armed, a request carrying an Origin gets CORS headers back:

$ curl -i -H "Origin: https://example.com" http://127.0.0.1:8000/index
HTTP/1.1 200 OK
access-control-allow-origin: https://example.com

With logging armed, requests appear in the server’s log output.

Gotchas

  • The registry enables errors; the shipped AsgiServer also arms session and auth. CORS, logging and wellknown require explicit configuration.

  • A call the handler cannot take is answered by the dispatcher, never a 500, on two distinct codes: a call that does not fit the signature — an unknown keyword, a missing required argument, one positional too many — answers 400, while values the signature accepts and the handler’s validation rejects answer 422 Unprocessable Content (RFC 9110 §15.5.21: the request is well formed, its semantics are not). What the handler BODY raises is mapped to neither and reaches errors as a 500; a handler raising HTTPBadRequest itself still answers 400, never remapped.

  • An unknown middleware name in middleware={...} raises ValueError. If you are arming a custom stage, register it in middleware_registry first.

  • Ordering is by priority, lower = more outer. A custom stage lands according to its own priority in the chain.

  • Session and auth are already armed on AsgiServer; configure their middleware options when needed, or explicitly disable them.