O tratamento de erros em aplicações FastAPI envolve muito mais do que devolver um código HTTP e uma mensagem genérica. Em uma API, erros representam contratos quebrados, dados inválidos, falhas internas e decisões de negócio, e por isso precisam de uma estrutura previsível e de boa observabilidade.
Uma abordagem avançada combina três pilares: padronização das respostas de erro, contexto (metadados úteis para diagnóstico) e observabilidade (logs, rastreamento e captura de exceções em produção). Esse conjunto reduz ambiguidades, melhora a integração com frontends e facilita investigações após incidentes.
Erros padrão no FastAPI e por que evoluir
O FastAPI, por padrão, devolve respostas JSON para exceções não tratadas e falhas de validação. Falhas de validação normalmente resultam em status 422, com uma lista de detalhes indicando onde o erro ocorreu e qual regra foi violada. Esse formato é correto tecnicamente, mas pode ser extenso e pouco amigável para consumidores de API. Em APIs maiores, surge a necessidade de uma estrutura consistente, com campos estáveis e mensagens mais diretas.
Também se torna importante distinguir erros de validação, erros de autorização, erros de regra de negócio e falhas internas. Cada categoria exige códigos HTTP adequados e mensagens que não exponham detalhes sensíveis. Além disso, a mesma aplicação pode atender múltiplos clientes, e a previsibilidade do formato de erro reduz complexidade no lado do consumidor. A evolução do tratamento padrão geralmente começa pela criação de um “contrato de erro” unificado.
Conceitos essenciais: exceção, status HTTP e contrato de erro
Uma exceção é um evento que interrompe o fluxo normal do programa, geralmente sinalizando falhas ou condições inesperadas. Um status HTTP é um código numérico que descreve o resultado de uma requisição, como 400 (requisição inválida) ou 500 (erro interno). Um contrato de erro é um formato estável de resposta que descreve como a API comunica falhas, incluindo campos como mensagem, tipo e contexto. Esses conceitos trabalham juntos para manter a API coerente e fácil de integrar.
Um contrato de erro bem definido evita respostas diferentes para o mesmo tipo de problema. Isso também permite instrumentação uniforme, como logs e captura em ferramentas de observabilidade. Outro benefício é a capacidade de adicionar metadados sem quebrar integrações existentes. Em geral, o contrato deve separar o que é “seguro para o cliente” do que é “útil para diagnóstico interno”.
Padronização de resposta de erro com esquema consistente
Uma resposta padronizada costuma incluir campos fixos para identificar o erro e oferecer contexto mínimo. Campos comuns são: tipo do erro, mensagem resumida, caminho do endpoint e um identificador de correlação. Esse identificador ajuda a localizar rapidamente o evento nos logs e em sistemas de monitoramento. A padronização também facilita a criação de documentação e testes automatizados.
Um padrão simples e prático pode ser: error (nome curto), detail (descrição), path (rota), request_id (correlação) e timestamp. Mesmo quando a API opta por mensagens amigáveis, detalhes internos devem ficar apenas nos logs. Esse cuidado reduz risco de vazamento de informações, como stack traces e dados sensíveis. A consistência do formato é mais importante do que a quantidade de campos.
Criando exceções de aplicação (erros de regra de negócio)
Erros de regra de negócio são falhas “esperadas”, como “e-mail já cadastrado” ou “saldo insuficiente”, e não devem ser tratados como falhas internas. Para isso, é comum definir uma exceção própria, com nome e detalhes controlados. Esse tipo de exceção evita o uso excessivo de mensagens genéricas e concentra a lógica de formatação do erro em um único lugar. O resultado é uma API mais previsível e fácil de manter.
O exemplo a seguir mostra uma exceção customizada e um handler dedicado, devolvendo status 400 com campos padronizados. O handler recebe o objeto Request, que carrega informações da requisição, como URL e headers. A resposta é construída via JSONResponse, que permite controlar status e conteúdo. Esse padrão pode ser expandido para diferentes códigos, como 404 para “recurso não encontrado” ou 409 para “conflito”.
from datetime import datetime, timezone
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class ErroDeNegocio(Exception):
def __init__(self, nome: str, detalhe: str, status_code: int = 400):
self.nome = nome
self.detalhe = detalhe
self.status_code = status_code
@app.exception_handler(ErroDeNegocio)
async def handler_erro_de_negocio(request: Request, exc: ErroDeNegocio):
request_id = getattr(request.state, "request_id", None)
return JSONResponse(
status_code=exc.status_code,
content={
"error": exc.nome,
"detail": exc.detalhe,
"path": request.url.path,
"request_id": request_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
Personalizando erros de validação (422) do Pydantic
O FastAPI usa Pydantic para validar dados de entrada, como body JSON, query params e path params. Quando a validação falha, ocorre RequestValidationError, normalmente retornando uma estrutura detalhada com localização e tipo do erro. Esse detalhamento é útil para depuração, mas pode ser verboso para aplicações cliente. Uma alternativa é normalizar o conteúdo para um formato compacto, mantendo apenas o essencial.
Um formato comum é uma lista de erros com o campo e a mensagem, além de um resumo geral. Essa escolha facilita a exibição de mensagens em interfaces e reduz acoplamento a detalhes internos do Pydantic. Ainda assim, o status 422 deve ser mantido para representar “entidade não processável”, isto é, dados sintaticamente válidos, mas semanticamente inválidos conforme o schema. O handler abaixo exemplifica essa normalização, preservando path e request_id.
from datetime import datetime, timezone
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def handler_validacao(request: Request, exc: RequestValidationError):
request_id = getattr(request.state, "request_id", None)
erros = []
for err in exc.errors():
loc = err.get("loc", [])
campo = loc[-1] if loc else "desconhecido"
erros.append(
{
"field": str(campo),
"error": err.get("msg", "valor inválido"),
}
)
return JSONResponse(
status_code=422,
content={
"error": "VALIDACAO",
"message": "Requisição inválida",
"errors": erros,
"path": request.url.path,
"request_id": request_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
Middleware para contexto: request_id e correlação de requisições
Em produção, um erro raramente é analisado isoladamente, pois costuma depender de sequência de eventos e de logs distribuídos. Um request_id (também chamado de correlation id) é um identificador que acompanha toda a requisição e aparece em respostas e logs. Ele pode vir de um header, como X-Request-ID, ou ser gerado quando não existe. Esse identificador permite localizar rapidamente a trilha de execução associada ao problema.
O FastAPI permite criar middleware, que é um componente executado antes e depois do endpoint. Esse middleware pode extrair o request_id do header, guardar em request.state e também devolvê-lo em um header de resposta. Essa estratégia reduz o esforço de depuração e melhora integrações com gateways e proxies. O exemplo abaixo gera um UUID quando necessário e garante consistência.
import uuid
from fastapi import Request
@app.middleware("http")
async def middleware_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID")
if not request_id:
request_id = str(uuid.uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
Handler global para exceções inesperadas (500) sem vazar detalhes
Exceções inesperadas indicam falhas internas, como bugs, indisponibilidade de dependências ou erros de infraestrutura. Nesses casos, a resposta ao cliente deve ser genérica, evitando expor stack traces, nomes de classes internas ou detalhes do ambiente. Ao mesmo tempo, logs internos precisam registrar o máximo de contexto seguro para diagnóstico. Esse equilíbrio reduz risco de segurança e melhora a capacidade de resposta a incidentes.
Um handler global para Exception captura erros não previstos e devolve status 500. É importante não “engolir” exceções HTTP já tratadas pelo framework, como HTTPException, que deve continuar seguindo seu fluxo normal. Uma forma simples é criar um handler para Exception e manter handlers específicos para categorias importantes. O exemplo abaixo registra no log e retorna uma mensagem padronizada.
import logging
from datetime import datetime, timezone
from fastapi import Request
from fastapi.responses import JSONResponse
logger = logging.getLogger("uvicorn.error")
@app.exception_handler(Exception)
async def handler_erro_inesperado(request: Request, exc: Exception):
request_id = getattr(request.state, "request_id", None)
logger.error(
"Erro inesperado na aplicação",
exc_info=exc,
extra={
"request_id": request_id,
"path": request.url.path,
"method": request.method,
},
)
return JSONResponse(
status_code=500,
content={
"error": "ERRO_INTERNO",
"message": "Falha interna ao processar a requisição",
"path": request.url.path,
"request_id": request_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
Logging com metadados: o que registrar e o que evitar
Logs eficientes precisam de contexto suficiente para reconstruir o cenário do erro. Informações como método HTTP, path, query params e request_id são particularmente valiosas para correlacionar eventos. Também é útil registrar tempos e, quando aplicável, identificadores internos não sensíveis. Em contrapartida, dados pessoais e segredos não devem aparecer em logs, como senhas, tokens, números de cartão e documentos.
O corpo da requisição (request body) pode conter informações sensíveis, então seu registro deve ser evitado ou rigorosamente sanitizado. Mesmo em casos de depuração, o ideal é logar apenas chaves relevantes e mascarar valores. Em ambientes concorrentes, como aplicações assíncronas, a configuração do logger deve ser consistente para evitar mensagens truncadas ou sem contexto. O uso de request_id reduz a necessidade de logar excesso de dados.
Observabilidade com Sentry: captura de exceções e contexto
Sentry é uma plataforma de monitoramento de erros que agrega exceções, stack traces e contexto de execução. Em APIs, ela ajuda a detectar regressões e incidentes rapidamente, agrupando erros semelhantes e exibindo frequência e impacto. A integração pode capturar exceções não tratadas e associar dados como rota, headers selecionados e identificadores de correlação. Isso reduz o tempo de investigação e melhora a qualidade do suporte operacional.
Uma integração típica inicializa o SDK com o DSN do projeto e adiciona middleware ASGI para instrumentar o ciclo de requisição. O DSN é um identificador de conexão que aponta para o projeto correto no Sentry. Também é comum configurar taxa de amostragem para tracing e performance, evitando excesso de eventos. O exemplo abaixo mostra uma configuração direta, mantendo a aplicação pronta para capturar exceções.
import sentry_sdk
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from fastapi import FastAPI
sentry_sdk.init(
dsn="SEU_DSN_DO_SENTRY",
traces_sample_rate=1.0,
)
app = FastAPI()
app.add_middleware(SentryAsgiMiddleware)
Rastreamento com OpenTelemetry: traces e diagnósticos distribuídos
OpenTelemetry é um conjunto de padrões e bibliotecas para coletar métricas, logs e rastreamentos. Em rastreamento, um trace representa a jornada de uma requisição por múltiplos componentes, e um span é uma etapa dentro dessa jornada, como uma chamada a banco de dados. Esse tipo de visibilidade é especialmente útil quando a API depende de outros serviços, filas ou provedores externos. A correlação entre spans ajuda a identificar gargalos e pontos de falha.
A instrumentação do FastAPI com OpenTelemetry costuma ser feita via bibliotecas de instrumentação ASGI/FastAPI, que criam spans automaticamente por requisição. Em um cenário completo, traces são exportados para um coletor ou backend de observabilidade compatível. O uso de request_id continua relevante, mas o trace_id passa a ser uma identificação ainda mais robusta para correlação distribuída. A combinação de tracing com logs estruturados oferece diagnósticos mais rápidos e confiáveis.
Erros contextuais: mensagens claras sem expor detalhes internos
Respostas de erro mais úteis incluem contexto mínimo que ajude a entender a falha sem revelar implementação. Códigos e mensagens consistentes permitem que clientes ajustem comportamento, como exibir mensagens, tentar novamente ou corrigir dados. Metadados como timestamp e request_id ajudam a auditoria e a investigação. Em APIs corporativas, também pode existir um “código interno” de erro, estável entre versões.
Uma prática comum é separar “message” (texto genérico) de “detail” (informação específica), usando detail apenas quando não houver risco. Em erros 500, detail geralmente não deve existir na resposta, ficando restrito a logs e ferramentas como Sentry. Em erros 4xx, detail pode explicar o motivo, como um campo ausente ou uma regra violada. Essa separação reduz vazamentos e mantém a API mais segura e profissional.
Erros comuns em tratamento avançado e como evitar
Um erro frequente é “engolir” exceções, isto é, capturar tudo e devolver 200 ou mensagens vagas, o que impede detecção de falhas e quebra contratos. Outro problema é devolver stack traces ao cliente, o que expõe caminhos internos, bibliotecas e detalhes do ambiente. A inconsistência de formato, com cada endpoint retornando campos diferentes, cria complexidade e aumenta custo de manutenção. Também é comum adiar observabilidade, resultando em baixa visibilidade quando o primeiro incidente ocorre.
Handlers globais precisam ser cuidadosos para não interferir em erros HTTP já tratados adequadamente. Logs devem ser úteis sem armazenar dados sensíveis, e o controle de acesso a logs deve ser considerado parte da segurança. Em sistemas concorrentes, a ausência de correlação torna a investigação lenta e sujeita a erro. Uma arquitetura consistente de exceções, middleware e handlers reduz esses problemas de forma significativa.
Organização do projeto: utilitários, handlers e middleware
À medida que a aplicação cresce, concentrar tudo em um único arquivo dificulta evolução e testes. Uma organização comum separa schemas de resposta, handlers de exceção e middleware de contexto/logging. Esse formato ajuda a reaproveitar padrões entre serviços e reduz duplicação. Também facilita a criação de testes que validam que o contrato de erro permanece estável.
A estrutura a seguir exemplifica uma separação simples, onde cada módulo tem responsabilidade única. “schemas.py” concentra modelos e formatos, “handlers.py” define tratadores e “middleware.py” aplica contexto e logging. O “main.py” registra tudo, mantendo o ponto de entrada limpo. Essa organização também ajuda na revisão de código, pois mudanças em tratamento de erro ficam centralizadas.
- error_utils/handlers.py com handlers de exceção (validação, negócio, global).
- error_utils/middleware.py com middleware de request_id e logging.
- error_utils/schemas.py com estruturas padronizadas de erro.
- main.py com registro de middleware e handlers.
Conclusão: de respostas 400/422 a uma API observável e previsível
Um tratamento de erros avançado em FastAPI combina formatação consistente, exceções de negócio bem definidas, handlers especializados e um handler global seguro para falhas inesperadas. A inclusão de request_id e timestamps fornece rastreabilidade, enquanto logs com metadados oferecem base sólida para diagnóstico. A personalização de erros 422 torna a validação mais amigável e reduz atrito com consumidores da API.
Quando observabilidade é adicionada com ferramentas como Sentry e rastreamento via OpenTelemetry, erros deixam de ser eventos opacos e passam a ser sinais acionáveis. Essa maturidade reduz tempo de investigação, diminui risco de vazamento de informações e melhora a estabilidade operacional. O resultado é uma API com comportamento previsível, comunicação clara de falhas e capacidade real de sustentação em produção. Essas práticas encerram um ciclo completo de tratamento de erros, desde a resposta ao cliente até a análise interna e a prevenção de recorrências.