Middleware
The openviper.middleware package provides built-in ASGI middlewares for
cross-cutting concerns: CORS, CSRF protection, rate limiting, security
headers, database connection pinning, error handling, and authentication.
All middlewares follow the standard ASGI callable protocol and can be
composed freely.
Overview
Middlewares are registered in settings.MIDDLEWARE (a tuple of dotted
import paths) or attached programmatically by wrapping the ASGI app. Each
middleware accepts the next app as its first constructor argument.
The stack is applied inner-first: the first entry in the list wraps the outermost layer (first to receive the request, last to process the response).
Key Classes
openviper.middleware.cors
- class CORSMiddleware(app, allowed_origins=None, allow_credentials=None, allowed_methods=None, allowed_headers=None, expose_headers=None, max_age=None)
Adds
Access-Control-*headers and handles preflightOPTIONSrequests.allowed_origins- list of allowed origin strings. Supports"*"(all) and wildcard patterns such as"https://*.example.com". Defaults to["*"]. WhenNone, readsCORS_ALLOWED_ORIGINSfrom project settings.allow_credentials- setAccess-Control-Allow-Credentials: trueand allowAuthorizationheaders / cookies cross-origin. Defaults toFalse(CORS_ALLOW_CREDENTIALSsetting). Cannot be combined with wildcard origins - aValueErroris raised to prevent credential leakage.allowed_methods- permitted request methods. Defaults to["*"](all methods). ReadsCORS_ALLOWED_METHODSsetting whenNone.allowed_headers- permitted request headers. Defaults to["*"](all headers). ReadsCORS_ALLOWED_HEADERSsetting whenNone.expose_headers- list of headers to expose to the browser viaAccess-Control-Expose-Headers. Defaults to[].max_age- preflight response cache TTL in seconds. Defaults to600(CORS_MAX_AGEsetting).
Origin patterns are compiled to
re.Patternobjects at__init__time so no per-request regex compilation occurs. Exact-match origins are stored in afrozensetfor O(1) lookup.When the response depends on the request origin (i.e. not wildcard), a
Vary: Originheader is appended to prevent cache poisoning by shared caches (CDNs).
openviper.middleware.csrf
- class CSRFMiddleware(app, secret='', cookie_name='csrftoken', header_name='x-csrftoken', exempt_paths=None)
Double-submit cookie CSRF protection for unsafe methods (
POST,PUT,PATCH,DELETE).The CSRF token is stored in a cookie (
csrftoken) and must be re-submitted in either:The
X-CSRFTokenrequest header, orThe
csrfmiddlewaretokenform field (application/x-www-form-urlencodedbodies only; body reads are capped at 2 MB to prevent DoS).
GET,HEAD, andOPTIONSrequests pass through without validation.secret- signing secret. Defaults tosettings.SECRET_KEY.cookie_name- name of the CSRF cookie.header_name- name of the HTTP header to check.exempt_paths- list of path strings that skip CSRF validation (e.g.["/api/webhooks"]).
Token verification uses HMAC-SHA256 with a per-request salt (128-bit) and
hmac.compare_digestfor constant-time comparison.Origin verification: When the request carries an
Originheader, it is checked againstsettings.CSRF_TRUSTED_ORIGINS. Trusted origins bypass the double-submit check, allowing cross-origin POST requests from known frontends.Cookie security settings:
Setting
Default
Description
CSRF_COOKIE_SECUREFalseAppend
Secureflag (HTTPS only).CSRF_COOKIE_HTTPONLYFalseAppend
HttpOnlyflag (no JS access).CSRF_COOKIE_SAMESITELaxSameSiteattribute (Lax,Strict,None).CSRF_COOKIE_AGENoneMax-Agein seconds.None= session cookie.
openviper.middleware.ratelimit
- class RateLimitMiddleware(app, max_requests=None, window_seconds=None, key_func=None)
Sliding-window rate limiter using 256 independent lock stripes for high concurrency.
max_requests- requests allowed per window (default:RATE_LIMIT_REQUESTSsetting, or 2000).window_seconds- time window in seconds (default:RATE_LIMIT_WINDOWsetting, or 60).key_func- callable(scope) -> strthat returns the bucket key. Defaults based onRATE_LIMIT_BYsetting (see below).
Responds with 429 Too Many Requests when the limit is exceeded and sets
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset, andRetry-Afterheaders on every response.Key strategies (
RATE_LIMIT_BYsetting):"ip"(default) - client IP from the ASGIclienttuple.X-Forwarded-Foris intentionally not used to prevent spoofing."user"- authenticated user primary key; falls back to IP for anonymous requests."path"-(client IP, request path)tuple; rate-limits each endpoint independently.
Backend (
RATE_LIMIT_BACKENDsetting):"memory"(default) - in-processSlidingWindowCounterwith per-stripe locking and lazy bucket eviction."redis"-RedisWindowCounterbacked by a Redis sorted set. Requiresredis>=7.4.0(install withpip install redis). UsesCACHES['default']['OPTIONS']['url']as the Redis connection URL.
- openviper.middleware.ratelimit.rate_limit(max_requests=60, window_seconds=60.0)
Per-view rate limit decorator (alternative to the middleware). Limits by client IP. Raises
TooManyRequests(429) when exceeded.
openviper.middleware.security
- class SecurityMiddleware(app, ssl_redirect=None, hsts_seconds=None, hsts_include_subdomains=None, hsts_preload=None, x_frame_options=None, content_type_nosniff=True, xss_filter=None, csp=None, permissions_policy=None, cross_origin_opener_policy=None, cross_origin_embedder_policy=None, cross_origin_resource_policy=None)
Adds HTTP security headers to every response and validates the
Hostheader againstsettings.ALLOWED_HOSTS.Host header validation: Requests with a disallowed host or header-control bytes (
\\r,\\n,\\0) in the host receive a 400 Bad Request response. This prevents host-header injection, CRLF response splitting, and cache poisoning.Headers added unconditionally (when the corresponding option is enabled):
X-Content-Type-Options: nosniff(content_type_nosniff=True)X-Frame-Options: DENY(configurable; set to""to disable)Referrer-Policy: strict-origin-when-cross-origin
Conditional headers:
Strict-Transport-Security: max-age=N[; includeSubDomains][; preload]- whenhsts_seconds > 0.HTTP to HTTPS redirect - when
ssl_redirect=True. Redirect targets are validated for CRLF bytes to prevent response splitting.Content-Security-Policy- when csp is provided as a dict or string. Semicolons in CSP dict keys/values are stripped as a defense against header injection.Permissions-Policy- when permissions_policy is provided.Cross-Origin-Opener-Policy- when cross_origin_opener_policy is provided.Cross-Origin-Embedder-Policy- when cross_origin_embedder_policy is provided.Cross-Origin-Resource-Policy- when cross_origin_resource_policy is provided.
All parameters default to
Noneand fall back to theirSECURE_*/X_FRAME_OPTIONSsettings counterparts.Deprecated:
xss_filterdefaults toNone(disabled). TheX-XSS-Protectionheader is removed from modern browsers and its legacy IE implementation introduced XSS vectors. UseContent-Security-Policyinstead. When explicitly enabled, a deprecation warning is logged.CSP dict example:
SecurityMiddleware( app, csp={ "default-src": "'self'", "script-src": "'self' https://cdn.example.com", "style-src": "'self' 'unsafe-inline'", }, )
openviper.middleware.db
- class DatabaseMiddleware(app)
Pins a single pooled database connection for the lifetime of each HTTP/WebSocket request via
ContextVar. All ORM calls within the request reuse the same connection, reducing pool checkout overhead and ensuring consistent reads underREAD COMMITTEDor stricter isolation.Non-HTTP scopes (lifespan, etc.) pass through without connection pinning.
No constructor arguments beyond
app. Connection management is handled byopenviper.db.connection.request_connection().
openviper.middleware.error
- class ServerErrorMiddleware(app, *, debug=False)
Outermost ASGI middleware that catches all unhandled exceptions and converts them into HTTP 500 responses.
debug=True- returns a rich HTML traceback page with CSP andX-Content-Type-Options: nosniffheaders for developer diagnostics.debug=False- logs the exception atERRORlevel and returns a plain500 Internal Server Errortext body that exposes no internals.
If
http.response.startwas already sent downstream when the exception occurs, the error can only be logged (no replacement response can be sent) and the exception is re-raised.This middleware does not extend
BaseMiddleware; it manages its own__call__and__slots__directly.
openviper.middleware.auth
- class AuthenticationMiddleware(app)
See Authentication & Authorization for full documentation. Populates
request.userandrequest.authfrom the configured auth backends.
openviper.middleware.base
- class BaseMiddleware(app)
Convenience base class for custom middlewares. Subclass and override
__call__.
- openviper.middleware.base.build_middleware_stack(app, middleware_classes) ASGIApp
Wrap app with a stack of ASGI middleware classes. middleware_classes is a list of either class objects (
CORSMiddleware) or(cls, kwargs_dict)tuples for classes that need arguments. Middlewares are applied inner-first (first item = outermost layer).
Example Usage
See also
Working projects that configure middleware:
examples/ai_moderation_platform/ - CORS, Security, and Auth middleware
examples/ecommerce_clone/ - Security, CORS, and Auth middleware stack
Registering via Settings
When middleware classes are listed in MIDDLEWARE, OpenViper automatically
reads the corresponding settings and passes them as constructor arguments -
no manual wiring required.
CORS settings (CORSMiddleware):
Setting |
Default |
Description |
|---|---|---|
|
|
Allowed origin strings or wildcard patterns ( |
|
|
Set |
|
all methods |
Permitted HTTP methods. |
|
|
Permitted request headers. |
|
|
Headers exposed to the browser via |
|
|
Preflight cache TTL in seconds. |
CSRF settings (CSRFMiddleware):
Rate-limit settings (RateLimitMiddleware):
Setting |
Default |
Description |
|---|---|---|
|
|
Maximum requests per window. |
|
|
Window duration in seconds. |
|
|
Key strategy: |
|
|
Counter backend: |
Security settings (SecurityMiddleware):
Setting |
Default |
Description |
|---|---|---|
|
|
Redirect all HTTP requests to HTTPS. |
|
|
HSTS |
|
|
Include |
|
|
Include |
|
|
|
|
|
Deprecated. Adds |
|
|
CSP as a dict or string. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Permitted |
import dataclasses
from openviper.conf import Settings
@dataclasses.dataclass(frozen=True)
class MySettings(Settings):
MIDDLEWARE: tuple = (
"openviper.middleware.security.SecurityMiddleware",
"openviper.middleware.cors.CORSMiddleware",
"openviper.middleware.db.DatabaseMiddleware",
"openviper.middleware.csrf.CSRFMiddleware",
"openviper.middleware.ratelimit.RateLimitMiddleware",
"openviper.auth.middleware.AuthenticationMiddleware",
)
# CORS
CORS_ALLOWED_ORIGINS: tuple = ("https://frontend.example.com",)
CORS_ALLOW_CREDENTIALS: bool = True
CORS_ALLOWED_METHODS: tuple = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
CORS_ALLOWED_HEADERS: tuple = ("Content-Type", "Authorization", "X-CSRFToken")
CORS_EXPOSE_HEADERS: tuple = ("X-Request-Id",)
CORS_MAX_AGE: int = 3600
# Security
SECURE_SSL_REDIRECT: bool = True
SECURE_HSTS_SECONDS: int = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS: bool = True
SECURE_HSTS_PRELOAD: bool = True
ALLOWED_HOSTS: tuple = (".example.com",)
# CSRF
CSRF_TRUSTED_ORIGINS: tuple = ("https://frontend.example.com",)
CSRF_COOKIE_SECURE: bool = True
CSRF_COOKIE_SAMESITE: str = "Lax"
# Rate limiting
RATE_LIMIT_REQUESTS: int = 200
RATE_LIMIT_WINDOW: int = 60
RATE_LIMIT_BY: str = "ip"
Programmatic Composition
from openviper import OpenViper
from openviper.middleware.cors import CORSMiddleware
from openviper.middleware.csrf import CSRFMiddleware
from openviper.middleware.db import DatabaseMiddleware
from openviper.middleware.security import SecurityMiddleware
from openviper.middleware.ratelimit import RateLimitMiddleware
from openviper.auth.middleware import AuthenticationMiddleware
app = OpenViper()
app = SecurityMiddleware(
app,
hsts_seconds=31536000,
hsts_include_subdomains=True,
hsts_preload=True,
ssl_redirect=True,
x_frame_options="SAMEORIGIN",
csp={"default-src": "'self'"},
)
app = CORSMiddleware(
app,
allowed_origins=["https://frontend.example.com"],
allow_credentials=True,
expose_headers=["X-Request-Id"],
max_age=3600,
)
app = CSRFMiddleware(app, exempt_paths=["/api/webhooks"])
app = DatabaseMiddleware(app)
app = RateLimitMiddleware(app, max_requests=200, window_seconds=60)
app = AuthenticationMiddleware(app)
Using build_middleware_stack
from openviper.middleware.base import build_middleware_stack
from openviper.middleware.cors import CORSMiddleware
from openviper.middleware.ratelimit import RateLimitMiddleware
app = build_middleware_stack(core_app, [
(CORSMiddleware, {"allowed_origins": ["*"]}),
(RateLimitMiddleware, {"max_requests": 100, "window_seconds": 60}),
])
CSRF Token in Forms
Include the CSRF token in HTML forms:
<form method="post" action="/submit">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
...
</form>
For AJAX requests, read the csrftoken cookie and send it as the
X-CSRFToken header:
const csrfToken = document.cookie.match(/csrftoken=([^;]+)/)?.[1];
fetch("/api/data", {
method: "POST",
headers: {"X-CSRFToken": csrfToken, "Content-Type": "application/json"},
body: JSON.stringify({key: "value"}),
});
Rate Limiting a Single View
from openviper.middleware.ratelimit import rate_limit
@router.post("/auth/login")
@rate_limit(max_requests=5, window_seconds=60)
async def login(request: Request) -> JSONResponse:
# At most 5 login attempts per IP per minute
...
Custom Middleware
import time
import uuid
from openviper.http.request import Request
from openviper.middleware.base import BaseMiddleware
class TimingMiddleware(BaseMiddleware):
async def __call__(self, scope, receive, send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
start = time.perf_counter()
await self.app(scope, receive, send)
elapsed = time.perf_counter() - start
print(f"{scope['path']} took {elapsed * 1000:.1f}ms")
class RequestIdMiddleware(BaseMiddleware):
"""Attach a unique request ID to every request's state."""
async def __call__(self, scope, receive, send) -> None:
if scope["type"] == "http":
req = Request(scope, receive)
req.state["request_id"] = str(uuid.uuid4())
await self.app(scope, receive, send)