fastware v0.1.0 /src.fastware.middleware
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: Optional ErrorLog instance. 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 set Access-Control-Allow-Credentials.

#_cors_headers

python
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

python
def _is_api_request(self, path: str) -> bool

Return True if this path should go to the app, not be proxied.

#close

python
async def close(self) -> None

#_proxy_http

python
async def _proxy_http(self, scope: Scope, send: Send, *, body: bytes=b'') -> None

Forward 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

python
async def _proxy_ws(self, scope: Scope, receive: Receive, send: Send) -> None

Bidirectional 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.