Skip to the content.

Middleware

Middleware lets you run code before and after your route handlers — for logging, authentication, security headers, rate limiting, and more.

axion supports two styles:

Both are registered through app.use(...). Middleware can be applied to the whole app, to a path prefix, or to a single route.

Callable-next style

A plain function that calls next_call() to continue the chain:

from axion import WebServer

app = WebServer(debug=True)


@app.use()
def timing(request, response, next_call):
    import time

    start = time.perf_counter()
    next_call()
    elapsed = (time.perf_counter() - start) * 1000
    print(f"{request.method} {request.path} took {elapsed:.2f}ms")
@app.use(methods=["GET"])
def auth(request, response, next_call):
    if request.headers.get("authorization") != "Bearer secret":
        response.send("Unauthorized", 401)
        return
    next_call()

Hook-object style

Subclass BaseMiddleware and override the hooks you need:

from axion.middleware import BaseMiddleware


class SecurityHeadersMiddleware(BaseMiddleware):
    def before_request(self, request, response):
        response.set_header("X-Frame-Options", "DENY")
        response.set_header("Referrer-Policy", "strict-origin-when-cross-origin")


app.use(SecurityHeadersMiddleware())

Registration order

Middleware runs in registration order. Inside the chain, the built-in middleware is applied first (CORS, then CSRF), followed by your app.use(...) registrations in order. Path-scoped middleware runs before route-level middleware for the matched route.

Scoping

Global — every request

app.use(SecurityHeadersMiddleware())

Path-scoped — requests under a prefix

app.use("/api", SecurityHeadersMiddleware())

/api also matches /api/users, but not /apix (whole-segment prefixes). A prefix of / matches every request.

Per-route — a single route

@app.route("/admin", middleware=[admin_check])
def admin(response):
    ...

Filtering by HTTP method

Restrict when middleware runs:

app.use(rate_limiter, methods=["POST"])
app.use("/uploads", csrf_protect, methods=["POST", "PUT", "DELETE"])

BaseMiddleware subclasses can declare a methods class attribute instead:

class LoggerMiddleware(BaseMiddleware):
    methods = ["GET", "HEAD"]

    def before_request(self, request, response):
        print(f"GET {request.path}")

When both are given, the use() argument wins.

Decorator form

use() doubles as a decorator when the middleware is a callable that also wraps the handler (Django-style middleware classes with __call__):

from axion.middleware import BaseMiddleware


class MyMiddleware(BaseMiddleware):
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request, response, next_call):
        return self.get_response(request, response, next_call)


@app.use(methods=["GET"])
@MyMiddleware
def my_view(response):
    ...

Configuration-driven middleware

List middleware by dotted path in settings and it is loaded automatically. Middlewares are imported left to right; the first entry is the outermost.

# settings.py
MIDDLEWARE = [
    "my_app.middleware.SecurityHeadersMiddleware",
]

What the hook object sees

Hooks receive the Request and the Response — exactly what your route handlers receive. Reading is always safe; write to the Response from before_request (headers, cookies) or short-circuit a request by sending a response and skipping next_call().

Next steps