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.
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))
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,
)
)
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))
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())
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"))
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())
RequestIDMiddleware
¶
Bases: Middleware
Assign a unique request ID to each request and echo it in the response.
Usage::
app.add_middleware(RequestIDMiddleware())
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))
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.
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.
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
¶
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
- ASGI scope
"scheme"if set to"https"/"wss"(the server already terminated TLS). X-Forwarded-Protoheader (when aProxyFix-style middleware ran upstream this is already the trusted value).- 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.
RateLimitMiddleware
¶
Bases: Middleware
Per-client rate limiter with a selectable algorithm and backend.
Two ways to configure it:
- The default
max_requestsperwindow_secondsruns a process-local sliding-log limiter - simple, zero-dependency, intended for a single worker. Counters are NOT shared across workers, souvicorn --workers Nsees roughlyN x max_requestsper window. - Pass a
strategy-FixedWindow,SlidingWindow, orTokenBucket- to choose the algorithm, and abackendto choose where state lives:InMemoryRateLimitBackend(default) orveloce.contrib.redis.RedisRateLimitBackendfor 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)},
)
)
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- passhsts_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- passcontent_security_policy.Permissions-Policy- passpermissions_policy.
A header a handler already set on the response is left untouched - these are defaults, not overrides.
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
¶
Resolved exclusion name - the instance/class name or class name.
process_response
async
¶
Called after route handler. Can modify the response.
is_host_allowed
¶
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.
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
¶
Resolved exclusion name - the instance/class name or class name.
process_response
async
¶
Called after route handler. Can modify the response.
is_websocket_origin_allowed
¶
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.