Skip to content

Middleware

The middleware base classes and every middleware shipped with the framework.

Middleware

Base middleware class. Subclass and override process_request/process_response.

Each middleware carries a name used by per-route exclusion (exclude_middleware=[...] on a route). The default name is the concrete class name; override the class attribute, or pass name= when two instances of the same class must be addressed independently.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Called before route handler. Return a Response to short-circuit.

process_response async

process_response(request: Request, response: Response) -> Response

Called after route handler. Can modify the response.

BaseHTTPMiddleware

Class-based dispatch-shape middleware.

Subclass and override dispatch, or construct with dispatch=fn for a one-off middleware. The instance is callable as (request, call_next) -> response, so it composes with the existing @app.middleware("http") chain.

Usage::

class TimingMW(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        start = time.perf_counter()
        response = await call_next(request)
        response.headers["X-Elapsed-ms"] = str(
            int((time.perf_counter() - start) * 1000)
        )
        return response

app.add_http_middleware(TimingMW())

# Or, without subclassing:
async def my_dispatch(request, call_next): ...
app.add_http_middleware(BaseHTTPMiddleware(dispatch=my_dispatch))

dispatch async

dispatch(request: Request, call_next: CallNext) -> Response

Override this in subclasses. Default just calls through.

Implementations must await call_next(request) exactly once to reach the wrapped handler.

CORSMiddleware

Bases: Middleware

Cross-Origin Resource Sharing middleware.

Usage::

app.add_middleware(
    CORSMiddleware(
        allow_origins=["https://example.com"],
        allow_methods=["GET", "POST"],
        allow_credentials=True,
    )
)

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Handle CORS preflight requests and validate origins.

process_response async

process_response(request: Request, response: Response) -> Response

Add CORS response headers.

GZipMiddleware

Bases: Middleware

GZip compression for responses above a size threshold.

Compression runs in the thread pool executor to avoid blocking the event loop.

Usage::

app.add_middleware(GZipMiddleware(minimum_size=1024, compresslevel=6))

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Called before route handler. Return a Response to short-circuit.

process_response async

process_response(request: Request, response: Response) -> Response

Compress the response body with gzip if the client accepts it.

ConditionalGetMiddleware

Bases: Middleware

Emit 304 responses for satisfied GET/HEAD preconditions.

With auto_etag (default), a weak ETag is synthesized for a buffered, non-empty, non-streaming 200 response that lacks one (unless Cache-Control: no-store is set). Register this AFTER GZipMiddleware so a synthesized/forwarded ETag reflects the post-compression bytes; StreamingResponse bodies are intentionally not buffered for synthesis.

Usage::

app.add_middleware(GZipMiddleware())
app.add_middleware(ConditionalGetMiddleware())

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Called before route handler. Return a Response to short-circuit.

CSRFMiddleware

Bases: Middleware

Double-submit-cookie CSRF middleware.

Issues a token cookie and requires an unsafe request to echo it back in a header or form field; a request whose two copies disagree is refused.

cookie_name / header_name / form_field rename the slots the token travels in, and safe_methods overrides which verbs bypass the check. cookie_secure / cookie_httponly / cookie_samesite set the cookie attributes - httponly must stay False because client-side JavaScript has to read the cookie to echo it, while secure defaults to True, so local HTTP development needs cookie_secure=False. Setting secret additionally HMAC-signs the token, which proves the value was minted by this server; the module docstring covers what that does and does not stop.

Usage::

from veloce import Veloce
from veloce.middleware.csrf import CSRFMiddleware

app = Veloce()
app.add_middleware(CSRFMiddleware(secret="a-long-random-string"))

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Validate the CSRF token on state-changing requests.

process_response async

process_response(request: Request, response: Response) -> Response

Set or rotate the CSRF cookie.

rotate_csrf_token

rotate_csrf_token(request: Request) -> None

Force the active CSRFMiddleware to mint a fresh token on response.

Call this at the end of an authentication handler (login, logout, permission elevation) so the CSRF cookie issued to the pre-authentication session is replaced by a fresh one bound to the new authentication state. Without rotation an attacker who plants a known CSRF cookie on an anonymous victim can submit forged requests after the victim logs in (session-fixation pathway).

Usage::

@app.post("/login")
async def login(request: Request):
    user = authenticate(...)
    request.session["user_id"] = user.id
    rotate_csrf_token(request)
    return RedirectResponse("/")

No-op when CSRFMiddleware is not installed.

LoggingMiddleware

Bases: Middleware

Structured request/response access logging.

Usage::

app.add_middleware(LoggingMiddleware())

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Record the request start time for duration logging.

process_response async

process_response(request: Request, response: Response) -> Response

Log the request method, path, status, and timing.

RequestIDMiddleware

Bases: Middleware

Assign a unique request ID to each request and echo it in the response.

Usage::

app.add_middleware(RequestIDMiddleware())

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Attach a unique request ID to each request.

process_response async

process_response(request: Request, response: Response) -> Response

Echo the request ID in the response headers.

ProxyFix

Bases: Middleware

Reverse-proxy header trust middleware.

Trusts N hops for each X-Forwarded-* header (right-to-left). Setting any field to 0 disables it. Negative values raise at construction.

x_port trusts X-Forwarded-Port: the resolved port fills in the public port for request.url / redirects when the forwarded Host carries none, so a proxy on a non-default port (e.g. 8443) is preserved. An explicit port in the Host / X-Forwarded-Host always wins.

Usage::

# Behind two trusted proxies forwarding client IP and scheme.
app.add_middleware(ProxyFix(x_for=2, x_proto=1, x_host=1))

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_response async

process_response(request: Request, response: Response) -> Response

Called after route handler. Can modify the response.

process_request async

process_request(request: Request) -> Response | None

Rewrite request attributes from trusted proxy headers.

SessionMiddleware

Bases: Middleware

Server-side session stored in a signed, timestamped cookie.

Constructor arguments left out fall back to the app's config on the first request: secret_key to SECRET_KEY (also settable as app.secret_key), cookie_name to SESSION_COOKIE_NAME, path to APPLICATION_ROOT, httponly/secure/samesite to the SESSION_COOKIE_* keys, permanent_lifetime to PERMANENT_SESSION_LIFETIME, and max_cookie_size to MAX_COOKIE_SIZE. An explicit argument always wins over config. Without either a secret_key= argument or a configured SECRET_KEY, the first request raises.

Set renew_on_access=True for sliding expiry: a session that was only read during a request has its cookie re-signed with a fresh Max-Age on the way out, so an active user is not logged out at the fixed max_age. Default is off - only a modifying write rewrites the cookie.

Set chunked=True to transparently split a signed value larger than max_cookie_size across numbered cookies (<cookie_name>.0, .1, ...) and reassemble them on the next request. max_chunks bounds the split so an oversized session is dropped with a warning rather than exploded into an unbounded number of cookies. Default is off - the single oversized cookie is dropped with a warning, unchanged from before.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Load the session from the signed cookie into request state.

process_response async

process_response(request: Request, response: Response) -> Response

Save the modified session back into the signed cookie.

ServerSessionMiddleware

Bases: Middleware

Server-side session - the cookie carries only an opaque session id.

The session payload lives in a SessionStore, not in the cookie, so a session is revocable: empty it in a handler (session.clear()) or delete it straight from the store (await store.delete(session_id)) and it is gone server-side. A tampered or stale cookie simply fails to resolve to a stored payload and is treated as a fresh session.

The default store is a process-local InMemorySessionStore; pass a shared backend (e.g. a Redis-backed SessionStore) for a multi-worker deployment. The store is a plain object the caller owns - keep a reference to it to revoke sessions by id.

Set renew_on_access=True for sliding expiry: a session that was only read during a request has its store TTL refreshed (via SessionStore.touch) and its cookie re-stamped on the way out - an idle-timeout reset. Default off.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Load the session from the server-side store by cookie id.

process_response async

process_response(request: Request, response: Response) -> Response

Save the modified session back to the server-side store.

CSPMiddleware

Bases: Middleware

Emit Content-Security-Policy with optional per-request nonce.

policy and report_only_policy each accept a str template containing the literal {nonce} placeholder, or a directive mapping where the 'nonce' source is substituted with a fresh per-request nonce.

Usage::

app.add_middleware(
    CSPMiddleware(
        policy={"default-src": "'self'", "script-src": ["'self'", "'nonce'"]},
        report_only_policy="default-src 'self'",
    )
)

Static (no-nonce) policies can stay on SecurityHeadersMiddleware; use this when a per-request nonce or a report-only policy is needed.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

csp_nonce

csp_nonce(request: Request | None = None) -> str | None

Return the per-request CSP nonce, materializing it on first access.

Templating helpers and handlers embed this on <script>/<style> tags as nonce="...". Pass the request explicitly, or omit it to read the one currently being handled. Returns None when CSPMiddleware did not arm a nonce for this request, or when there is no request in scope.

HTTPSRedirectMiddleware

Bases: Middleware

Redirect HTTP requests to HTTPS.

Resolves the request scheme in this order
  1. ASGI scope "scheme" if set to "https"/"wss" (the server already terminated TLS).
  2. X-Forwarded-Proto header (when a ProxyFix-style middleware ran upstream this is already the trusted value).
  3. Default http.

Uses 308 Permanent Redirect (RFC 9110 Sec. 15.4.9) so non-GET methods preserve their method and body. The earlier 301 form was wrong for POST/PUT callers - those would silently become GET.

Pass exempt_paths=("/health/", ...) to serve some paths over plain HTTP (prefix match - use a trailing slash to scope to a segment). By default /.well-known/acme-challenge/ is exempt (RFC 8555 Sec. 8.3: the HTTP-01 challenge MUST be reachable over plain HTTP for certificate issuance and renewal); pass exempt_acme_challenge=False to drop that default.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_response async

process_response(request: Request, response: Response) -> Response

Called after route handler. Can modify the response.

process_request async

process_request(request: Request) -> Response | None

Redirect HTTP requests to HTTPS with a 308 status.

RateLimitMiddleware

Bases: Middleware

Per-client rate limiter with a selectable algorithm and backend.

Two ways to configure it:

  • The default max_requests per window_seconds runs a process-local sliding-log limiter - simple, zero-dependency, intended for a single worker. Counters are NOT shared across workers, so uvicorn --workers N sees roughly N x max_requests per window.
  • Pass a strategy - FixedWindow, SlidingWindow, or TokenBucket - to choose the algorithm, and a backend to choose where state lives: InMemoryRateLimitBackend (default) or veloce.contrib.redis.RedisRateLimitBackend for one limit shared across every worker and host.

Give a route its own limit by decorating its handler with rate_limit - the limit lives on the handler, so there is no route string to mistype::

from veloce import rate_limit

@app.post("/login")
@rate_limit(TokenBucket(rate=5, per=60))
async def login(request): ...

The overrides map is the central alternative for handlers you cannot decorate: it maps a route's full path template to a strategy. The key is the template as matched at runtime - the value of request.url_rule - so a blueprint route includes its url_prefix (/api/login, not /login); an override key that matches no route raises on the first request. An explicit overrides entry wins over a rate_limit tag on the same route.

Either way, an overridden route gets its own per-client counter, independent of the default budget; routes without an override keep the shared default. Like exclude_middleware, the per-route strategy is resolved against the route matched at dispatch entry, so a before_request hook that rewrites the path does not change which limit applies.

Usage::

from veloce import RateLimitMiddleware, TokenBucket

app.add_middleware(
    RateLimitMiddleware(
        strategy=TokenBucket(rate=1000, per=60),
        overrides={"/login": TokenBucket(rate=5, per=60)},
    )
)

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Enforce per-client request rate limits.

process_response async

process_response(request: Request, response: Response) -> Response

Attach X-RateLimit-* headers to successful responses.

SecurityHeadersMiddleware

Bases: Middleware

Attach common hardening response headers to every response.

Set by default:

  • X-Content-Type-Options: nosniff - stop MIME sniffing.
  • X-Frame-Options: DENY - block framing (clickjacking).
  • Referrer-Policy: strict-origin-when-cross-origin.

Off unless configured:

  • Strict-Transport-Security - pass hsts_max_age (seconds). Browsers honour HSTS only over HTTPS, so it is inert in plain-HTTP development, but it is still opt-in because it pins clients to HTTPS for the configured lifetime.
  • Content-Security-Policy - pass content_security_policy.
  • Permissions-Policy - pass permissions_policy.

A header a handler already set on the response is left untouched - these are defaults, not overrides.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_request async

process_request(request: Request) -> Response | None

Called before route handler. Return a Response to short-circuit.

process_response async

process_response(request: Request, response: Response) -> Response

Attach security hardening headers to every response.

TrustedHostMiddleware

Bases: Middleware

Validates Host header against an allow-list.

Supports literal hostnames, the catch-all *, and subdomain wildcards of the form *.example.com (matches api.example.com, a.b.example.com, etc. - never the bare example.com). Matching is case-insensitive; the port portion of Host: is stripped before comparison (RFC 9110 Sec. 7.2).

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_response async

process_response(request: Request, response: Response) -> Response

Called after route handler. Can modify the response.

is_host_allowed

is_host_allowed(host: str) -> bool

Whether host (bare hostname, no port) passes the allow-list.

Public so the WebSocket dispatch path can apply the same check - a WebSocket handshake never reaches an HTTP middleware's process_request.

process_request async

process_request(request: Request) -> Response | None

Reject requests whose Host header is not in the allow-list.

WebSocketOriginMiddleware

Bases: Middleware

Reject cross-site WebSocket handshakes (CSWSH).

A WebSocket handshake is not subject to the Same-Origin Policy and bypasses CORS entirely, so a page on any origin can open a socket to your app unless the handshake Origin is checked. Register this with the origins your own front-end is served from; a handshake whose Origin is present but unlisted is refused with close code 1008.

Browsers always send Origin on a WebSocket handshake (RFC 6455 Sec. 4.1), so allow_missing=True (the default) still blocks every browser-driven CSWSH attempt while leaving non-browser clients (mobile apps, service-to-service) - which legitimately omit Origin - able to connect. Set allow_missing=False to additionally refuse handshakes that carry no Origin at all.

Plain HTTP requests pass straight through - Origin enforcement for HTTP is CORSMiddleware's job.

middleware_name property

middleware_name: str

Resolved exclusion name - the instance/class name or class name.

process_response async

process_response(request: Request, response: Response) -> Response

Called after route handler. Can modify the response.

is_websocket_origin_allowed

is_websocket_origin_allowed(origin: str) -> bool

Whether a handshake carrying origin may proceed.

Public so the WebSocket dispatch path can apply the check - a handshake never reaches an HTTP middleware's process_request.