On this page
Pure ASGI middleware for request tracing, CORS headers, trusted-host validation, and Vite dev proxy routing, all streaming-safe for SSE and WebSocket.
#src.fastware.middleware
#src.fastware.middleware
Pure ASGI middleware for request tracing, CORS headers, trusted-host validation, and Vite dev proxy routing, all streaming-safe for SSE and WebSocket.
All middleware classes are pure ASGI -- no framework dependency beyond fastware's own send_error helper. This keeps them streaming-safe (SSE, WebSocket) and avoids the response-buffering issues of BaseHTTPMiddleware-style wrappers.
Classes: RequestIDMiddleware — assigns/propagates X-Request-Id per request RequestTimingMiddleware — logs method, path, status, duration; ring buffer CORSMiddleware — preflight OPTIONS + response header injection TrustedHostMiddleware — rejects requests from unlisted Host headers ViteDevProxy — proxies non-API requests to a Vite dev server
#RequestIDMiddleware
Assign or propagate a unique request ID per request.
If the incoming request carries an X-Request-Id header, that value is reused. Otherwise a new UUID4 is generated. The ID is stored in scope["state"]["request_id"] and returned as an X-Request-Id response header.
When structlog is available, contextvars are cleared at the start of each request (preventing context leak from a previous request) and the request ID is bound so all log entries within the request include it.
#RequestTimingMiddleware
Log every HTTP request with method, path, status, and duration.
Wraps send to capture the status code from http.response.start, then uses try/finally so timing fires even for long-lived SSE streams (when the client disconnects the ASGI handler returns).
Args:
app: The inner ASGI application.error_log: OptionalErrorLoginstance. On 5xx responses the
middleware calls error_log.append(...) to persist the failure for dashboard visibility.
exclude_paths: Iterable of path prefixes to exclude from the ring
buffer (e.g. ["/events"]). Excluded requests are still logged, just not stored.
maxlen: Maximum number of entries in the ring buffer (default 10000).
#CORSMiddleware
Add CORS headers to responses and handle preflight OPTIONS requests.
Args:
app: The inner ASGI application.allow_origins: List of allowed origins (e.g.["http://localhost:5173"]).
Use ["*"] to allow any origin.
allow_methods: HTTP methods to advertise. Defaults to common methods.allow_headers: Request headers the client may send. Defaults to
common headers.
allow_credentials: Whether to setAccess-Control-Allow-Credentials.
#_cors_headers
def _cors_headers(self, origin: str) -> list[tuple[bytes, bytes]]Build the list of CORS response headers for origin.
#TrustedHostMiddleware
Reject requests whose Host header is not in the allow-list.
Prevents DNS rebinding attacks for servers bound to localhost.
Args:
app: The inner ASGI application.allowed_hosts: List of hostnames (with optional port) to allow.
Use ["*"] to disable the check.
#ViteDevProxy
ASGI middleware that proxies unmatched requests to a Vite dev server.
Uses a backend-first routing strategy for HTTP: every request hits the backend first. If the backend returns 404 (no route matched), the request is proxied to Vite instead. This means backend routes like /health or /metrics work without being under an API prefix.
WebSocket upgrades cannot be retried, so they still use prefix-based routing: paths matching api_prefix or /events go to the backend; everything else is proxied to Vite (for HMR).
Args:
app: The inner ASGI application.vite_port: Port the Vite dev server is listening on.api_prefix: Path prefix for backend WebSocket routes (default
"/api"). Only used for WebSocket routing decisions.
#_is_api_request
def _is_api_request(self, path: str) -> boolReturn True if this path should go to the app, not be proxied.
#close
async def close(self) -> None#_proxy_http
async def _proxy_http(self, scope: Scope, send: Send, *, body: bytes=b'') -> NoneForward an HTTP request to the Vite dev server.
The body parameter contains the pre-captured request body (already consumed from receive by the backend during the try-first phase).
#_proxy_ws
async def _proxy_ws(self, scope: Scope, receive: Receive, send: Send) -> NoneBidirectional WebSocket proxy to Vite (for HMR).
Uses the websockets library if available. Falls back to closing the connection with an error code if not installed.