Skip to content

Middleware

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

Middleware

Bases: Auditable

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_request async

process_request(request: Request) -> Response | None

Run before the route handler; return a Response to short-circuit.

process_response async

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

Run after the route handler; may modify the response.

BaseHTTPMiddleware

Bases: Auditable

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))

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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.

CallNext module-attribute

CallNext = Callable[[Request], Awaitable[Response]]

DispatchFunction module-attribute

DispatchFunction = Callable[[Request, CallNext], Awaitable[Response]]

CORSMiddleware

Bases: Middleware

Cross-Origin Resource Sharing middleware.

Usage::

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

allow_headers defaults to ["*"], and the Fetch standard forbids a wildcard alongside credentials, so it must be listed explicitly whenever allow_credentials=True - omitting it raises at construction.

middleware_name property

middleware_name: str

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

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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: CompressionMiddleware

GZip compression for responses above a size threshold.

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

Offers gzip and nothing else, so a client asking for brotli or zstd is served an uncompressed body. Use CompressionMiddleware to negotiate across the newer codings.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_request async

process_request(request: Request) -> Response | None

Run before the route handler; return a Response to short-circuit.

process_response async

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

Compress the response body with the coding the client accepts.

CompressionMiddleware

Bases: Middleware

Response compression, negotiated over the codings the server can emit.

Offers zstd, brotli and gzip - whichever of their packages are installed - and picks one per response from Accept-Encoding. The client's q weights rank the candidates; algorithms order breaks ties, so a deployment states its own preference for clients that express none.

Compression above min_stream_chunk_offload bytes runs in the thread pool to avoid holding the event loop.

Usage::

from veloce import CompressionMiddleware

app.add_middleware(CompressionMiddleware(algorithms=("br", "gzip")))

middleware_name property

middleware_name: str

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

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_request async

process_request(request: Request) -> Response | None

Run before the route handler; return a Response to short-circuit.

process_response async

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

Compress the response body with the coding the client accepts.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_request async

process_request(request: Request) -> Response | None

Run before the 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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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.

trust_forwarded opts into RFC 7239 Forwarded, which supersedes the X-Forwarded-* set and is then the sole authority for every directive it carries. Enable it only where every trusted proxy sets or sanitizes Forwarded itself: nginx, ALB and most CDNs emit X-Forwarded-* and leave Forwarded untouched, so a client-supplied header would otherwise decide the client address, scheme and host - and silence the header the proxy does control.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_response async

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

Run after the route handler; may modify the response.

process_request async

process_request(request: Request) -> Response | None

Rewrite request attributes from trusted proxy headers.

SessionMiddlewareBase

Bases: Middleware

Base class for a session middleware — subclass it to add a backend.

Subclassing supplies two things a backend would otherwise have to reimplement. It inherits the documented session.permanent rule (a permanent session takes the longer lifetime), and it becomes the type Veloce.security_audit recognises, so veloce check warns when the backend's session cookie is not Secure. A backend that does not subclass gets neither, and the audit passes it in silence.

A subclass sets max_age and permanent_lifetime, then calls cookie_lifetime(session) wherever it writes the cookie or the store entry. Both built-in backends - SessionMiddleware (signed cookie) and ServerSessionMiddleware (server-side store) - are subclasses, and adding another requires no edit inside the framework.

Usage::

from veloce import SessionMiddlewareBase

class RedisSessionMiddleware(SessionMiddlewareBase):
    def __init__(self, client, max_age=3600, permanent_lifetime=2592000):
        self.client = client
        self.max_age = max_age
        self.permanent_lifetime = permanent_lifetime

    async def process_response(self, request, response):
        session = request.session
        ttl = self.cookie_lifetime(session)
        await self.client.setex(session.sid, ttl, session.serialize())
        response.set_cookie("session", session.sid, max_age=ttl, secure=True)
        return response

middleware_name property

middleware_name: str

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

wire_cookie_name: str

The cookie name actually used on the wire.

cookie_name with the RFC 6265bis __Host- / __Secure- prefix applied. Anything writing or reading this backend's cookie directly needs this name and not the configured one - seeded under the bare name the cookie is simply never found, and the read silently takes the anonymous path.

process_request async

process_request(request: Request) -> Response | None

Run before the route handler; return a Response to short-circuit.

process_response async

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

Run after the route handler; may modify the response.

cookie_lifetime

cookie_lifetime(session: Any) -> int

Return the lifetime this session's cookie and entry should carry.

cookie_is_secure

cookie_is_secure() -> bool

Whether this backend's session cookie will carry Secure.

Answered from the middleware alone - there is no second source to reconcile. A subclass that never sets secure reads as not secure, so the audit reports a cookie it cannot vouch for rather than staying quiet about it.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report an insecure cookie, and config that no longer configures one.

SessionMiddleware

Bases: SessionMiddlewareBase

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

Every cookie setting comes from this constructor. An argument left out takes the library default and does not change again - app.config is not consulted, so what the signature says is what the cookie carries, and two session middlewares can carry different cookies.

secret_key is the exception: left out, it is taken from SECRET_KEY (also settable as app.secret_key) on the first request, because it is the application's signing key rather than an attribute of this cookie. Without either, 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.

wire_cookie_name: str

The cookie name actually used on the wire.

cookie_name with the RFC 6265bis __Host- / __Secure- prefix applied. Anything writing or reading this backend's cookie directly needs this name and not the configured one - seeded under the bare name the cookie is simply never found, and the read silently takes the anonymous path.

cookie_lifetime

cookie_lifetime(session: Any) -> int

Return the lifetime this session's cookie and entry should carry.

cookie_is_secure

cookie_is_secure() -> bool

Whether this backend's session cookie will carry Secure.

Answered from the middleware alone - there is no second source to reconcile. A subclass that never sets secure reads as not secure, so the audit reports a cookie it cannot vouch for rather than staying quiet about it.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report a backend that cannot sign, on top of the shared checks.

A cookie session is signed, so a middleware with neither a secret_key= argument nor a configured SECRET_KEY cannot serve a single request - it raises on the first one. That is an error, which refuses the boot, rather than a warning discovered by traffic.

The middleware answers this and not the audit, because only it knows whether the constructor supplied a key: reading SECRET_KEY alone warned about a backend that was already signing correctly.

bind_secret_key

bind_secret_key(config: Mapping[str, Any], *, hint: str = '') -> None

Settle the signing key from config if the constructor was not given one.

A no-op once the key is settled, so it is safe to call from anywhere that needs a usable signer before a request has run - seeding a session in a test, or a CLI command that mints one. hint is appended to the error raised when there is no key, so the caller can say when it needed it.

encode_cookie(session: Mapping[str, Any]) -> str

Return the signed cookie value this middleware would send for session.

Minting a valid session cookie outside a request - a test fixture that starts logged in, a tool that hands a user a pre-authenticated link - otherwise means rebuilding the signer with this middleware's exact secret and salt, and any drift in that construction produces cookies the middleware rejects.

decode_cookie(value: str) -> dict[str, Any] | None

Return the session inside a signed cookie value, or None if invalid.

None covers a bad signature, a tampered payload, and a token older than this middleware would accept. The request path calls this, so what it accepts and what a request accepts cannot diverge.

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: SessionMiddlewareBase

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.

wire_cookie_name: str

The cookie name actually used on the wire.

cookie_name with the RFC 6265bis __Host- / __Secure- prefix applied. Anything writing or reading this backend's cookie directly needs this name and not the configured one - seeded under the bare name the cookie is simply never found, and the read silently takes the anonymous path.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report an insecure cookie, and config that no longer configures one.

cookie_lifetime

cookie_lifetime(session: Any) -> int

Return the lifetime this session's cookie and entry should carry.

cookie_is_secure

cookie_is_secure() -> bool

Whether this backend's session cookie will carry Secure.

Answered from the middleware alone - there is no second source to reconcile. A subclass that never sets secure reads as not secure, so the audit reports a cookie it cannot vouch for rather than staying quiet about it.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_response async

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

Run after the route handler; may 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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report overrides keys that match no registered route.

A key matching nothing means a route the operator believes is throttled but is not. Under strict_overrides that is an error, so the boot fails naming the keys rather than the app serving unthrottled; otherwise it is a warning and the overrides are simply inactive.

Reads the route table, so audit_needs_routes keeps it from running against an app that was imported but never started - routes registered during startup are not there yet, and would read as missing.

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

Run before the route handler; return a Response to short-circuit.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Note the opt-in headers this instance is not sending.

The three defaults are always safe; Strict-Transport-Security and Content-Security-Policy are not, so they stay off until asked for. That leaves a registered instance looking hardened while sending neither, which is said here rather than left to a scanner to find. Informational: an app served over plain HTTP, or one whose policy is set at a proxy, is right to leave them off.

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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_response async

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

Run after the route handler; may 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.

audit

audit(ctx: AuditContext) -> Iterable[Finding]

Report what is wrong with this component's own configuration.

The audit collects these from everything registered, so a check belongs to the thing it is about and an app that registers none never loads the code that checks them. Return nothing when there is nothing to say.

Severity decides what a finding does: an error refuses the boot, a warning fails veloce check without stopping anything, and info fails nothing. Set audit_needs_routes when the check reads ctx.app's routes. Runs at audit and startup time only - never on a request path.

process_request async

process_request(request: Request) -> Response | None

Run before the route handler; return a Response to short-circuit.

process_response async

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

Run after the route handler; may 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.