Capítulo 14, Intermediário
Middleware
O middleware é uma camada que **toda** requisição atravessa, na ida e na volta. É o lugar certo para o que vale para a aplicação inteira: medir tempo, identificar a requisição, compactar respostas.
Como funciona
Um middleware recebe a requisição antes de ela chegar à rota e a resposta depois de a rota responder. O que vem antes do await call_next(request) roda na ida, e o que vem depois roda na volta. O call_next passa a requisição adiante:
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"}
O middleware mede o tempo gasto desde que a requisição entra até a resposta sair, e o devolve em um cabeçalho. Qualquer rota da aplicação ganha isso sem mudar uma linha. O middleware é async def porque roda no laço de eventos de todas as requisições.
Identificar cada requisição
Um identificador de requisição deixa você ligar uma reclamação do cliente aos logs do servidor. O middleware cria um (ou reaproveita o que veio de um proxy), guarda em request.state para a rota usar e o devolve no cabeçalho:
@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'}
Os dois middlewares agem juntos: a rota / ganhou o cabeçalho de tempo, e a /quem-sou leu o identificador que o outro middleware deixou em request.state.
Middlewares precisam estar prontos antes da primeira requisição
Se você tentar adicionar um middleware depois de a aplicação já ter respondido a uma requisição, o Starlette recusa com
RuntimeError: Cannot add middleware after an application has started. Em um programa real isso não aparece, porque tudo é declarado quando o módulo é importado. Por isso, neste capítulo, oTestClientsó é criado depois de os middlewares estarem declarados.
A ordem dos middlewares
Quando há vários, o último registrado fica por fora: ele é o primeiro a ver a requisição e o último a ver a resposta. Dá para ver o desenho:
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 que já vêm prontos
O FastAPI inclui os mais comuns. O GZipMiddleware, por exemplo, comprime respostas maiores que um tamanho mínimo, quando o cliente diz que aceita:
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
Outros que você vai usar: o CORSMiddleware (capítulo 22) e o TrustedHostMiddleware, que recusa requisições com um cabeçalho Host inesperado.
Middleware, dependência e tratador
| Middleware | Dependência | Tratador de exceção | |
|---|---|---|---|
| Alcance | Toda requisição | As rotas que a pedem | Quando uma exceção específica ocorre |
| Vê a resposta | Sim | Não | Cria a resposta de erro |
| Bom para | Tempo, ID de requisição, compressão, cabeçalhos | Autenticação, banco, validações por rota | Formato único de erro |
| Recebe parâmetros da rota | Não | Sim, com validação | Não |
Cuidado com o que o middleware faz
O middleware roda em todas as requisições, inclusive nas de
/docs. Trabalho lento ou bloqueante nele atrasa a aplicação inteira. E o@app.middleware("http")é construído sobreBaseHTTPMiddleware, que tem limitações com respostas em streaming. Para o caminho crítico de aplicações muito carregadas, existe o middleware ASGI puro, mais complexo, mas sem esse custo.
Exercício 1
Um cabeçalho de versão
Crie um middleware que acrescente X-App-Version: 1.0 a todas as respostas, e duas rotas para conferir.