Capítulo 12, Básico
Tratamento global de exceções
Em vez de espalhar `try` e `HTTPException` por todas as rotas, você registra um tratador em um lugar só, e **todo** erro da aplicação sai no mesmo formato.
Uma exceção do seu domínio
Uma exceção própria diz o que deu errado em termos do seu negócio, sem se preocupar com HTTP. A rota só a levanta. Quem a traduz para uma resposta é o tratador:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
app = FastAPI()
class UsuarioNaoEncontrado(Exception):
def __init__(self, nome: str):
self.nome = nome
@app.exception_handler(UsuarioNaoEncontrado)
async def tratar_usuario_nao_encontrado(request: Request, exc: UsuarioNaoEncontrado):
return JSONResponse(
status_code=404,
content={"erro": "usuario_nao_encontrado", "mensagem": f"Usuário {exc.nome} não existe"},
)
@app.get("/usuarios/{nome}")
def obter(nome: str):
if nome != "Mohit":
raise UsuarioNaoEncontrado(nome)
return {"nome": nome}
cliente = TestClient(app)
print(cliente.get("/usuarios/Mohit").json())
resposta = cliente.get("/usuarios/Rohit")
print(resposta.status_code, resposta.json())
{'nome': 'Mohit'}
404 {'erro': 'usuario_nao_encontrado', 'mensagem': 'Usuário Rohit não existe'}
Sem o tratador, a exceção chegaria ao cliente como um 500 genérico. Com ele, qualquer rota da aplicação que levante UsuarioNaoEncontrado produz a mesma resposta, sem repetir código.
Reformatar os erros de validação
Os erros 422 do FastAPI são ricos, mas despejam detalhes internos e repetem o valor enviado em input, que pode conter dados sensíveis. Você pode trocar o formato registrando um tratador para RequestValidationError:
from fastapi.exceptions import RequestValidationError
app_validacao = FastAPI()
@app_validacao.exception_handler(RequestValidationError)
async def tratar_validacao(request: Request, exc: RequestValidationError):
erros = [
{"campo": ".".join(str(p) for p in e["loc"][1:]), "mensagem": e["msg"]}
for e in exc.errors()
]
return JSONResponse(status_code=422, content={"erro": "dados_invalidos", "detalhes": erros})
@app_validacao.get("/produtos/{produto_id}")
def produto(produto_id: int, quantidade: int):
return {"produto_id": produto_id, "quantidade": quantidade}
print(TestClient(app_validacao).get("/produtos/abc?quantidade=x").json())
{'erro': 'dados_invalidos', 'detalhes': [{'campo': 'produto_id', 'mensagem': 'Input should be a valid integer, unable to parse string as an integer'}, {'campo': 'quantidade', 'mensagem': 'Input should be a valid integer, unable to parse string as an integer'}]}
O valor enviado (abc, x) não aparece mais na resposta: cada erro diz qual campo falhou e por quê.
Registre os tratadores antes da primeira requisição
O Starlette monta a pilha de tratadores e middlewares na primeira requisição. Um tratador registrado depois disso é ignorado em silêncio (um middleware tardio, ao contrário, levanta um erro, como você vai ver no capítulo 14). Em um programa real isso não acontece, porque tudo é registrado quando o módulo é importado, antes de o servidor aceitar conexões. Mas em um notebook ou em um teste que reaproveita o
appdepois de já tê-lo chamado, você pode perder uma tarde procurando o motivo. Por isso, cada demonstração deste capítulo usa o seu próprioapp.
Erros HTTP no mesmo formato
Tratar o HTTPException (da Starlette, a base do FastAPI) faz todos os seus erros comuns, inclusive os 404 de rota inexistente, saírem no mesmo formato. Aqui eu uso o formato padronizado RFC 9457 (application/problem+json), que muitas APIs adotam:
from starlette.exceptions import HTTPException as StarletteHTTPException
def problema(status: int, titulo: str, detalhe: str | None = None) -> JSONResponse:
corpo = {"type": "about:blank", "title": titulo, "status": status}
if detalhe:
corpo["detail"] = detalhe
return JSONResponse(corpo, status_code=status, media_type="application/problem+json")
app_http = FastAPI()
@app_http.exception_handler(StarletteHTTPException)
async def tratar_http(request: Request, exc: StarletteHTTPException):
return problema(exc.status_code, str(exc.detail))
resposta = TestClient(app_http).get("/rota-que-nao-existe")
print(resposta.status_code, resposta.headers["content-type"], resposta.json())
404 application/problem+json {'type': 'about:blank', 'title': 'Not Found', 'status': 404}
O último recurso: erros inesperados
Uma exceção que ninguém previu vira um 500. O cliente não deve ver o texto da exceção (pode vazar caminhos, trechos de SQL, nomes de bibliotecas). Registre um tratador para Exception que devolva uma mensagem neutra, e mande o detalhe para o log:
import logging
logger = logging.getLogger("app")
app_erro = FastAPI()
@app_erro.exception_handler(Exception)
async def tratar_inesperado(request: Request, exc: Exception):
logger.exception("Erro não tratado em %s", request.url.path)
return problema(500, "Erro interno do servidor")
@app_erro.get("/bug")
def bug():
return 1 / 0
sem_relancar = TestClient(app_erro, raise_server_exceptions=False)
resposta = sem_relancar.get("/bug")
print(resposta.status_code, resposta.json())
500 {'type': 'about:blank', 'title': 'Erro interno do servidor', 'status': 500}
A divisão por zero não apareceu na resposta. Um cliente só vê "Erro interno do servidor", e o motivo real foi para o log.
| Tratador | Captura | Para que |
|---|---|---|
| Exceção do domínio | Uma classe sua | Traduzir regras de negócio em HTTP em um lugar só |
RequestValidationError | Falhas de validação (422) | Padronizar o formato e não vazar o valor recebido |
StarletteHTTPException | Todo HTTPException e o 404 de rota inexistente | Formato único de erro |
Exception | O inesperado (500) | Esconder o detalhe e registrar no log |
Exercício 1
Traduzir um ValueError em 400
Registre um tratador para ValueError que devolva 400 com {"erro": "valor_invalido", "mensagem": <texto do erro>}, e uma rota GET /raiz/{numero} que levante ValueError("não existe raiz de número negativo") para números negativos.