Pular para o conteúdo

    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:

    basico/cap12_excecoes_globais.pylinhas 10 a 40
    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())
    
    Saída
    {'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:

    basico/cap12_excecoes_globais.pylinhas 45 a 64
    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())
    
    Saída
    {'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 app depois 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óprio app.

    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:

    basico/cap12_excecoes_globais.pylinhas 69 a 88
    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())
    
    Saída
    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:

    basico/cap12_excecoes_globais.pylinhas 93 a 112
    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())
    
    Saída
    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.

    TratadorCapturaPara que
    Exceção do domínioUma classe suaTraduzir regras de negócio em HTTP em um lugar só
    RequestValidationErrorFalhas de validação (422)Padronizar o formato e não vazar o valor recebido
    StarletteHTTPExceptionTodo HTTPException e o 404 de rota inexistenteFormato único de erro
    ExceptionO 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.