Middleware

Status: πŸ”΄ DA REVISIONARE

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. Most stages are off by default and armed with the middleware kwarg.

The built-in chain

The registry ships these middleware, with a priority and a default state. 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.

Two of these arm themselves as a side effect: passing session_store/session_ttl arms session, and passing auth=... arms auth. You rarely toggle those two directly.

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):
    @route()
    def index(self) -> dict:
        return {"ok": True}


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

CORS options

cors accepts an options dict:

server = AsgiServer(
    primary=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(
    primary=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

  • errors is the only stage on by default β€” it is why an unknown path is a clean 404 in the hello-world. The rest are off until you arm them.

  • 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.

  • Do not hand-arm session/auth when you are already passing session_store/auth β€” those kwargs arm the respective middleware for you.