Chapter 14, Intermediate
Middleware
The middleware is a layer that **every** request passes through, both on the way in and on the way out. It is the right place for what applies to the entire application: measuring time, identifying the request, compressing responses.
How it works
A middleware receives the request before before it reaches the route and the response after the route responds. What comes before the await call_next(request) runs on the way in, and what comes after runs on the way out. The call_next passes the request along:
import time
import uuid
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
app = FastAPI()
@app.middleware("http")
async def medir_tempo(request: Request, call_next):
inicio = time.perf_counter()
resposta = await call_next(request)
resposta.headers["X-Process-Time"] = f"{time.perf_counter() - inicio:.6f}"
return resposta
@app.get("/")
def home():
return {"mensagem": "ok"}
The middleware measures the time taken from when the request enters until the response goes out, and returns it in a header. Any route in the application gains this without changing a line. The middleware is async def because it runs in the event loop of all requests.
Identify each request
A request identifier allows you to link a customer complaint to the server logs. The middleware creates one (or reuses what came from a proxy), stores it in request.state for the route to use, and returns it in the header:
@app.middleware("http")
async def id_da_requisicao(request: Request, call_next):
request.state.request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
resposta = await call_next(request)
resposta.headers["X-Request-ID"] = request.state.request_id
return resposta
@app.get("/quem-sou")
def quem_sou(request: Request):
return {"request_id": request.state.request_id}
cliente = TestClient(app)
resposta = cliente.get("/")
print(resposta.status_code, float(resposta.headers["x-process-time"]) >= 0)
sem_id = cliente.get("/quem-sou")
print(len(sem_id.headers["x-request-id"]), sem_id.json()["request_id"] == sem_id.headers["x-request-id"])
print(cliente.get("/quem-sou", headers={"x-request-id": "abc-123"}).json())
200 True
36 True
{'request_id': 'abc-123'}
The two middlewares work together: the route / received the time header, and the /quem-sou read the identifier that the other middleware left in request.state.
Middlewares need to be ready before the first request
If you try to add a middleware after the application has already responded to a request, Starlette refuses with
RuntimeError: Cannot add middleware after an application has started. In a real program, this does not appear because everything is declared when the module is imported. Therefore, in this chapter, theTestClientis only created after the middlewares are declared.
The order of middlewares
When there are several, the the last registered is left out: he is the first to see the request and the last to see the response. You can see the drawing:
ordem = []
app_ordem = FastAPI()
@app_ordem.middleware("http")
async def primeiro(request: Request, call_next):
ordem.append("primeiro: ida")
resposta = await call_next(request)
ordem.append("primeiro: volta")
return resposta
@app_ordem.middleware("http")
async def segundo(request: Request, call_next):
ordem.append("segundo: ida")
resposta = await call_next(request)
ordem.append("segundo: volta")
return resposta
@app_ordem.get("/")
def raiz():
ordem.append("rota")
return {}
TestClient(app_ordem).get("/")
print(ordem)
['segundo: ida', 'primeiro: ida', 'rota', 'primeiro: volta', 'segundo: volta']
Middlewares that come ready-made
FastAPI includes the most common ones. The GZipMiddleware, for example, compresses responses larger than a minimum size when the client says it accepts:
from fastapi.middleware.gzip import GZipMiddleware
app_gzip = FastAPI()
app_gzip.add_middleware(GZipMiddleware, minimum_size=500)
@app_gzip.get("/grande")
def grande():
return {"texto": "repetição " * 500}
@app_gzip.get("/pequena")
def pequena():
return {"texto": "oi"}
gz = TestClient(app_gzip)
print(gz.get("/grande", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
print(gz.get("/pequena", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
gzip
None
Others you will use: the CORSMiddleware (chapter 22) and the TrustedHostMiddleware, which rejects requests with an unexpected header Host .
Middleware, dependency, and handler
| Middleware | Dependency | Exception handler | |
|---|---|---|---|
| Scope | Every request | The routes that request it | When a specific exception occurs |
| Sees the response | Yes | No | Creates the error response |
| Good for | Time, request ID, compression, headers | Authentication, database, route validations | Unique error format |
| Receives parameters from the route | No | Yes, with validation | No |
Be careful with what the middleware does
The middleware runs on all the requests, including those of
/docs. Slow or blocking work in it delays the entire application. And the@app.middleware("http")is built on top ofBaseHTTPMiddleware, which has limitations with streaming responses. For the critical path of heavily loaded applications, there is pure ASGI middleware, more complex, but without this cost.
Exercise 1
A version header
Create a middleware that adds X-App-Version: 1.0 to all the responses, and two routes to check.