Ready-made middleware¶
Use this when you want common protections (CORS, rate limiting, security
headers...) without writing them. korvex.middleware has ready-made
middleware: each is a function that registers a
hook on the app:
from korvex import App
from korvex.middleware import compress, cors, csrf, rate_limit, request_id, security_headers, sessions
app = App()
compress(app) # first: it compresses what the others produced
cors(app, allow_origins=["https://example.com"])
request_id(app)
rate_limit(app, requests=100, per=60)
security_headers(app)
csrf(app) # refuses cross-site form posts
sessions(app, secret_key="x" * 32) # signed cookie sessions; use a real secret key
Behind a reverse proxy, let the proxy compress and add headers. Sessions, flash messages and CSRF protection are in the Sessions and CSRF guide.
CORS: cors(app, allow_origins=...)¶
Lets browsers call the app from pages on other sites (origins): CORS.
| Option | Default | What it does |
|---|---|---|
allow_origins |
(required) | The sites allowed, like ["https://example.com"], or "*" for any. |
allow_methods |
all of them | The methods those pages may use. |
allow_headers |
none | Request headers they may send beyond the basic ones, or "*". |
allow_credentials |
False |
Whether they may send cookies. |
expose_headers |
none | Response headers their code may read. |
max_age |
600 |
Seconds the browser may remember a preflight answer. |
A browser's preflight request (OPTIONS with
Access-Control-Request-Method) is answered before routing: 204 if
allowed, 400 if not. Every other response to an allowed origin gets the
CORS headers, errors included.
Request IDs: request_id(app)¶
Gives each request an ID, sends it back in the response, and puts it in your logs.
| Option | Default | What it does |
|---|---|---|
header |
"X-Request-ID" |
The header the ID is read from and sent back in. |
trust_incoming |
True |
Keep an ID the client or a proxy sent, when it's safe (letters, digits, ._:-, up to 200). False always makes a new one. |
current_request_id()returns it anywhere in your code, while the request runs.RequestIdFilteradds it to log records:
import logging
from korvex.middleware import RequestIdFilter
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(logging.Formatter("%(request_id)s %(message)s"))
logging.getLogger("korvex").addHandler(handler)
Rate limiting: rate_limit(app, requests=100, per=60)¶
Allows each client requests requests every per seconds; beyond, a 429
with Retry-After.
| Option | Default | What it does |
|---|---|---|
requests, per |
(required) | The allowance, like 100 per 60 seconds. |
key |
the client's address | Who the limit applies to: a function of the request, returning an API key, a user ID... or None to exempt the request (a health check). |
store |
a MemoryStore |
Where the counts live: see below. |
headers |
True |
Send RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so clients can slow down. |
It counts WebSocket handshakes too. Requests can come in bursts of up to
requests, then one every per / requests seconds.
- Hooks declared before it run first: call
cors()before it, so browsers' preflight requests are answered without being counted. - Behind a proxy, trust it with
--forwarded-allow-ips, or every client counts as the proxy. Requests with no client address at all share one limit.
Where the counts live¶
MemoryStore(), the default: in each worker's memory. With 3 workers, a client gets up to 3 times the limit.RedisStore("redis://localhost:6379/0"): in Redis, shared by every worker and machine (pip install korvex[redis]). It takes a URL or aredis.asyncio.Redisyou made yourself;prefix=sets what its keys start with. If Redis can't be reached, requests go through and the error is logged;fail_open=Falserefuses them instead.
Your own store¶
A store (a RateLimitStore) is any object with a hit() method, sync
or async, that decides for one request:
from korvex.middleware import Decision
class MyStore:
async def hit(self, key, requests: int, per: float) -> Decision:
# your code: count the hit, in Redis, a database...
return Decision(allowed=True, remaining=99, retry_after=0, reset=0.6)
rate_limit(app, requests=100, per=60, store=MyStore()) # app: the one above
Decision has allowed, remaining (requests left), retry_after
(seconds until the next one is allowed, 0 if allowed) and reset
(seconds until the whole allowance is back). The store decides rather
than just storing numbers, so a shared one can do it atomically.
Compression: compress(app)¶
Compresses text-like responses for clients that accept it.
| Option | Default | What it does |
|---|---|---|
encodings |
("zstd", "br", "gzip") |
In order of preference, among those the client accepts. zstd needs Python 3.14, br the brotli package (pip install korvex[brotli]); those not available are skipped. On free-threaded Python, br is left out of the default: the brotli package turns the GIL back on. |
minimum_size |
500 |
Smaller bodies are sent as they are. |
levels |
gzip 6, brotli 4, zstd 3 | Per encoding: {"gzip": 9}. |
content_types |
text, JSON, JavaScript, XML, SVG... | What's worth compressing: prefixes like "text/", or suffixes like "+json". |
- Streams are compressed piece by piece, each piece sent at once, so a live stream isn't held back.
- A response that already has a
Content-Encoding, saysCache-Control: no-transform, or has no body is left alone. - Don't compress a page that holds a secret (a CSRF token) next to something the client sent: an attacker could guess the secret (the BREACH attack).
Security headers: security_headers(app)¶
Adds the headers that tell browsers to protect your pages. A response that sets one itself keeps its own.
| Option | Default | What it does |
|---|---|---|
hsts |
one year | Strict-Transport-Security: browsers stick to HTTPS. Sent over HTTPS only. |
hsts_subdomains, hsts_preload |
False |
Add those directives; only once every subdomain serves HTTPS. |
content_security_policy |
none | A CSP, like "default-src 'self'". Off by default: the right one depends on your pages, and /docs loads Swagger UI from a CDN. |
frame_options |
"DENY" |
X-Frame-Options: other sites can't show your pages in a frame. "SAMEORIGIN" allows your own. |
content_type_options |
True |
X-Content-Type-Options: nosniff: browsers trust Content-Type. |
referrer_policy |
"strict-origin-when-cross-origin" |
Referrer-Policy. |
cross_origin_opener_policy |
"same-origin" |
Cross-Origin-Opener-Policy. |
permissions_policy |
none | Permissions-Policy, like "camera=(), microphone=()". |
Each header option takes None to leave the header out.
All the details are in the reference.
Next: Testing.