Skip to content

    Chapter 12, Basic

    Global exception handling

    Instead of spreading `try` and `HTTPException` across all routes, you register a handler in one place, and **every** application error comes out in the same format.

    An exception from your domain

    A custom exception says what went wrong in terms of your business, without worrying about HTTP. The route just raises it. The handler translates it into a response:

    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())
    
    Output
    {'nome': 'Mohit'}
    404 {'erro': 'usuario_nao_encontrado', 'mensagem': 'Usuário Rohit não existe'}
    

    Without the handler, the exception would reach the client as a 500 generic. With it, any route in the application that raises UsuarioNaoEncontrado produces the same response, without repeating code.

    Reformat validation errors

    The errors 422 from FastAPI are rich, but they dump internal details and repeat the value sent in input, which may contain sensitive data. You can change the format by registering a handler for 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())
    
    Output
    {'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'}]}
    

    The sent value (abc, x) no longer appears in the response: each error states which field failed and why.

    Register the handlers before the first request

    Starlette builds the stack of handlers and middlewares on the first request. A handler registered after that is silently ignored (a late middleware, on the other hand, raises an error, as you will see in chapter 14). In a real program, this does not happen, because everything is registered when the module is imported, before the server accepts connections. But in a notebook or in a test that reuses the app after having already called it, you might spend an afternoon looking for the reason. Therefore, each demonstration in this chapter uses its own app.

    HTTP errors in the same format

    Handling the HTTPException (from Starlette, the base of FastAPI) makes all your common errors, including the 404 of non-existent routes, come out in the same format. Here I use the standardized format RFC 9457 (application/problem+json), which many APIs adopt:

    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())
    
    Output
    404 application/problem+json {'type': 'about:blank', 'title': 'Not Found', 'status': 404}
    

    The last resort: unexpected errors

    An exception that no one anticipated becomes a 500. The client should not see the exception text (it may leak paths, SQL snippets, library names). Register a handler for Exception that returns a neutral message, and send the detail to the 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())
    
    Output
    500 {'type': 'about:blank', 'title': 'Erro interno do servidor', 'status': 500}
    

    The division by zero did not appear in the response. A client only sees "Internal Server Error", and the real reason was logged.

    HandlerCaptureFor what
    Domain exceptionYour own classTranslate business rules into HTTP in one place
    RequestValidationErrorValidation failures (422)Standardize the format and do not leak the received value
    StarletteHTTPExceptionAll HTTPException and the 404 for non-existent routeUnique error format
    ExceptionThe unexpected (500)Hide the detail and log it

    Exercise 1

    Translate a ValueError into 400

    Register a handler for ValueError that returns 400 with {"erro": "valor_invalido", "mensagem": <texto do erro>}, and a route GET /raiz/{numero} that raises ValueError("não existe raiz de número negativo") for negative numbers.