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:
- Callable-next middleware: a callable that receives
(request, response, next_call). - Hook-object middleware: a class with
before_request/after_requesthooks.
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")
- Everything before
next_call()runs before the route handler. - Everything after
next_call()runs after the response has been sent. - To short-circuit, do not call
next_call()— send a response directly:
@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())
before_requestruns before the route handler. Because it can write to theResponse, this is where you set headers on every reply.after_requestruns after the route handler. The response has already been transmitted at this point, so it is for observation/cleanup only.
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
- Turn on CSRF and CORS, which ship as built-in middleware.
- See the example application’s
middleware.pyfor a working hook-object middleware.