Starlette Middleware Stack: ASGI Request Pipeline and Exception Handlers
- Last updated
- Prerequisites:
- ASGI specification (`scope`, `receive`, `send`)
- FastAPI architecture
- fastapi
- starlette
- asgi
- middleware
- routing
- pipeline
Read at your depth
The practical view
FastAPI is built directly on top of **Starlette**. Starlette handles the low-level ASGI request/response pipeline, HTTP routing, WebSockets, static file serving, and middleware. You add middleware using `@app.middleware('http')`: `async def add_process_time_header(request: Request, call_next): response = await call_next(request); return response`. Middleware executes in an onion-like stack (First-In, Last-Out).
The same idea in other frameworks
Starlette ASGI middleware `async def (request, call_next)`.Express middleware `(req, res, next)`.NestJS Middleware / Interceptor pipeline.Spring Security `FilterChain` and `OncePerRequestFilter`.Legacy vs modern
Raw WSGI Interception vs Starlette ASGI Async Middleware
WSGI middleware blocked synchronous threads; Starlette ASGI middleware runs non-blocking asynchronous request/response pipelines.
def __call__(self, environ, start_response): ... // ❌ Synchronous WSGI blocking@app.middleware('http') async def add_timing(req, call_next): ... // ✅ NEW: Non-blocking asyncInterview gotchas
Context
Interviewers test your knowledge of ASGI stream buffering gotchas.
The mechanical answer
`BaseHTTPMiddleware` (which powers `@app.middleware('http')`) abstracts request/response streams by buffering and wrapping them in an internal task group. In high-concurrency or large streaming response workloads, this introduces memory overhead and context switching. For extreme performance or SSE/WebSocket streaming, write a **pure ASGI middleware class** implementing `async def __call__(self, scope, receive, send)` directly.
Trap
`BaseHTTPMiddleware` can break background tasks or streaming responses if not carefully handled.
Context
Checks precision regarding the onion-architecture middleware execution order.
The mechanical answer
Middleware executes like an **onion**: the **last added middleware runs FIRST on the incoming request**, and runs **LAST on the outgoing response**. If you add Middleware A and then Middleware B (`app.add_middleware(A); app.add_middleware(B)`), the execution flow is: `Request -> B -> A -> Endpoint -> A -> B -> Response`.
Trap
Placing authentication middleware before CORS middleware will cause preflight `OPTIONS` requests to fail with 401 Unauthorized.