Middleware¶
FastAPI applications add middleware with app.add_middleware(...) or @app.middleware("http"): code that
runs around every request. The binary rebuilds Starlette's middleware stack in the same order (see
Handling errors for the layers around your middleware) and runs
BaseHTTPMiddleware subclasses, @app.middleware("http") functions and raw ASGI middleware classes of the
project.
A function middleware (@app.middleware("http")) and a BaseHTTPMiddleware subclass, both
adding a header to every response. Like every example on this site, it is compiled and compared with FastAPI in CI (how).
docs_src/main.py (excerpt)
@app.middleware("http")
async def add_served_by(request: Request, call_next):
response = await call_next(request)
response.headers["X-Served-By"] = "docs-example"
return response
class RequestIdMiddleware(BaseHTTPMiddleware):
"""Echo the caller's request id, or say there was none."""
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Request-Id"] = request.headers.get("x-request-id", "none")
return response
app.add_middleware(RequestIdMiddleware)
What is native¶
GZipMiddleware(Vary: Accept-Encodinglike Starlette),BaseHTTPMiddlewaresubclasses (one instance per app, built on the first request;call_next, mutableresponse.headers; with or without their own__init__),@app.middleware("http").CORSMiddlewarehas its own page.- Raw ASGI middleware: a project class with
__init__(self, app, ...)andasync def __call__(self, scope, receive, send), registered withapp.add_middleware(Cls, ...)orFastAPI(middleware=[Middleware(Cls, ...)])(those entries stay inside everyadd_middleware, in list order, as in Starlette). The instance is built with the rest of the stack as itsapp. Onescopedict per request, shared by every layer (and byrequest.scopebelow them);receive()returns the whole body in onehttp.requestmessage, thenhttp.disconnectonce the response is complete (uvicorn);send()takeshttp.response.start/http.response.body(more_bodystreams; withoutcontent-lengththe response is sent chunked, as uvicorn does).await self.app(scope, receive, send)runs the rest of the stack: a rewritten scope (headers,method,path,query_string) is a new request for it,scope["state"]isrequest.state, a wrappedreceiveis read to the end of the body and becomes the request body, the response goes through a wrappedsendas Starlette's messages (http.response.startwith the raw headers, one body message for a body of known size, one per part plus an empty last one when streamed), and an exception the stack lets through is raised in the middleware. The middleware can answer itself (send()messages, orawait response(scope, receive, send)); its code afterawait self.app(...)runs once the body is complete (finallyblocks included). An exception before the response starts reaches the outer middlewares, thenServerErrorMiddleware; after it started, the response is kept and the exception logged. ContextVars set aroundawait self.app(...)are seen by the endpoint and back.app.stateis a process-wideState(add_middleware(Cls, state=app.state)). starlette_context0.5'sRawContextMiddleware(plugins=(RequestIdPlugin(), CorrelationIdPlugin()))(plugin optionsforce_new_uuid,validate,version=4as literals): the id read from its header or a newuuid4().hex, validated likeuuid.UUID(value)(400 with an empty body otherwise), appended to the response headers.request.cookies(Starlette's parser),request.client(TCP peer),request.url,request.headers,request.state,request.body()/json().- A project function given the application from the factory or the app's module (
configure(app)) runs while the middleware stack is built (on the first request, when Starlette instantiates its middlewares): in it,app.add_middleware(ProjectMiddleware, ...)with a projectBaseHTTPMiddlewaresubclass or raw ASGI class,@app.exception_handler(...)on its nested functions,app.add_exception_handler(...),@app.middleware("http"),app.add_route(path, endpoint, methods=)(a StarletteRoute: GET implies HEAD; such routes are tried after the declared ones, as when added last),app.routes. - Starlette's
TrustedHostMiddleware(allowed_hosts=[...], www_redirect=...)(Host header parsed like Starlette 1.7:Invalid host header400,www.redirect) andHTTPSRedirectMiddleware(307 to the https URL, port dropped when 80 or 443), with literal options. HTTP requests only: with WebSocket routes they are refused (a WebSocket handshake does not cross the binary's middleware stack).
What stays in Python¶
- Raw ASGI middleware:
http.response.trailers/pathsend/zerocopymessages and a rewrittenroot_pathraise at run time. - The
contextobject ofstarlette_contextitself is not available. - In a
configure(app)function, other registrations than those above are refused. - An application wrapped in a project ASGI class at module level (
app = Wrapper(api)) is refused: see Not supported.
Differences¶
- Known difference:
HTTPSRedirectMiddlewarewith noHostheader or one Starlette cannot parse (or a port above 65535): Starlette redirects to the server's own address (scope["server"]), which the binary does not know; it answers 500 instead. - Differences (raw ASGI middleware):
GZipMiddlewarealways runs outermost, so a middleware added after it sees the response before compression; raw middlewares run forhttpscopes only (websocketandlifespanpass as if the middleware let them through, its usual first line); a wrappedreceiveis read to the end of the body before the rest of the stack runs, and an originalreceivealready consumed by an outer layer still yields the body (uvicorn would wait for the client to disconnect); the 500 for a middleware returning without a response is produced by the layer, so outer middlewares see it (uvicorn produces it itself); an exception raised once the response has started (by a middleware after itsawait self.app(...), or while the body streams) does not go back through the raw middlewares wrappingsendaround it, so their frames are missing from its traceback (Logging).
GZip and streamed responses: see Large list responses.