Capítulo 12: Middlewares Customizados, Tratamento Global de Exceções e CORS
Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Requisição HTTP chega"] --> B["Middleware 1: Correlation-ID"]
B --> C["Middleware 2: X-Process-Time"]
C --> D["Middleware 3: CORSMiddleware"]
D --> E["Roteamento / Handler da Rota"]
E -->|"exceção customizada"| F["@app.exception_handler"]
E -->|"sucesso"| G["Response"]
F --> H["JSONResponse padronizada"]
H --> D
G --> D
D --> C
C --> B
B --> I["Resposta final ao cliente"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style D fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style E fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style F fill:#ffebee,stroke:#e53935,stroke-width:2px
style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style I fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Middlewares, Exception Handlers e CORS: a casca transversal da API
Middlewares e exception handlers resolvem o mesmo problema por ângulos diferentes: evitar repetir lógica transversal em cada rota. Um middleware envolve toda requisição/resposta (logging, tempo de processamento, CORS); um exception handler intercepta um tipo específico de erro onde quer que ele seja levantado, centralizando a formatação da resposta de falha.
Anatomia de um middleware HTTP: call_next como bisonte de duas cabeças
Um middleware registrado com @app.middleware("http") recebe a Request e uma função call_next que, quando awaited, executa o restante da pilha (outros middlewares + a rota) e retorna a Response. Isso dá ao middleware dois pontos de interceptação na mesma função: antes de call_next (pode inspecionar/modificar a requisição) e depois (pode inspecionar/modificar a resposta):
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def adicionar_tempo_processamento(request: Request, call_next):
inicio = time.perf_counter()
response = await call_next(request)
duracao = time.perf_counter() - inicio
response.headers["X-Process-Time"] = f"{duracao:.4f}"
return response
A ordem de registro importa: middlewares são empilhados como uma cebola — o último registrado é o primeiro a processar a requisição de entrada e o último a processar a resposta de saída, já que ele “envolve” os middlewares registrados antes dele.
Correlation ID: rastreabilidade ponta a ponta
Em arquiteturas com múltiplos serviços, correlacionar logs de uma mesma requisição que atravessa vários processos exige um identificador único propagado ao longo de toda a cadeia. O padrão é extrair (ou gerar, se ausente) um X-Correlation-ID no primeiro middleware, injetá-lo no contexto de logging da aplicação e devolvê-lo no header de resposta, para que o cliente e os serviços downstream compartilhem a mesma referência:
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def correlation_id_middleware(request: Request, call_next):
correlation_id = request.headers.get("X-Correlation-ID", str(uuid.uuid4()))
request.state.correlation_id = correlation_id
response = await call_next(request)
response.headers["X-Correlation-ID"] = correlation_id
return response
request.state é o mecanismo correto para passar dados de um middleware para os handlers de rota dentro do mesmo ciclo de requisição — não existe variável global segura para isso em um servidor ASGI concorrente.
Exception handlers: erros de negócio viram respostas HTTP padronizadas
Levantar HTTPException em toda regra de negócio espalha a formatação de erro por dezenas de arquivos. A alternativa é definir exceções de domínio puras (sem qualquer conhecimento de HTTP) e registrar um @app.exception_handler que as traduz centralmente para uma JSONResponse:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class RecursoNaoEncontradoException(Exception):
def __init__(self, mensagem: str) -> None:
self.mensagem = mensagem
app = FastAPI()
@app.exception_handler(RecursoNaoEncontradoException)
async def recurso_nao_encontrado_handler(request: Request, exc: RecursoNaoEncontradoException):
return JSONResponse(
status_code=404,
content={"erro": "RECURSO_NAO_ENCONTRADO", "detalhe": exc.mensagem},
)
Isso mantém a camada de serviço (Service Layer) desacoplada do protocolo HTTP: uma função de domínio pode levantar RecursoNaoEncontradoException e ser reutilizada tanto por uma rota HTTP quanto por um worker de fila, sem carregar fastapi.HTTPException como dependência.
Uma decisão de design relevante: @app.exception_handler roda fora da cadeia normal de middlewares em versões mais antigas do Starlette (o erro é tratado antes de propagar pela pilha), o que significa que um middleware pode não ver a resposta de erro a menos que use BaseHTTPMiddleware ou trate a exceção via try/except explícito ao redor de call_next. Isso é uma armadilha comum ao combinar logging de resposta com exception handlers customizados — vale sempre testar o caminho de erro, não só o de sucesso.
CORS: middleware, não exception handler
CORS (Cross-Origin Resource Sharing) é resolvido inteiramente no navegador do cliente, mas o servidor precisa declarar explicitamente quem tem permissão de consumir a API a partir de outra origem, via headers de resposta. Em produção, allow_origins=["*"] combinado com allow_credentials=True é uma combinação inválida e insegura (a especificação CORS proíbe curinga com credenciais) — a lista de origens deve ser explícita:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.minhaempresa.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)