Ir para o conteúdo

Manipulação de erros

Há diversas situações em que você precisa notificar um erro a um cliente que está utilizando a sua API.

Esse cliente pode ser um browser com um frontend, o código de outra pessoa, um dispositivo IoT, etc.

Pode ser que você precise comunicar ao cliente que:

  • O cliente não tem direitos para realizar aquela operação.
  • O cliente não tem acesso aquele recurso.
  • O item que o cliente está tentando acessar não existe.
  • etc.

Nesses casos, você normalmente retornaria um HTTP status code próximo ao status code na faixa do status code 400 (do 400 ao 499).

Isso é bastante similar ao caso do HTTP status code 200 (do 200 ao 299). Esses "200" status codes significam que, de algum modo, houve sucesso na requisição.

Os status codes na faixa dos 400 significam que houve um erro por parte do cliente.

Você se lembra de todos aqueles erros (e piadas) a respeito do "404 Not Found"?

Use o HTTPException

Para retornar ao cliente responses HTTP com erros, use o HTTPException.

Import HTTPException

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

Lance o HTTPException no seu código.

HTTPException, ao fundo, nada mais é do que a conjunção entre uma exceção comum do Python e informações adicionais relevantes para APIs.

E porque é uma exceção do Python, você não retorna (return) o HTTPException, você lança o (raise) no seu código.

Isso também significa que, se você está escrevendo uma função de utilidade, a qual você está chamando dentro da sua função de operações de caminhos, e você lança o HTTPException dentro da função de utilidade, o resto do seu código não será executado dentro da função de operações de caminhos. Ao contrário, o HTTPException irá finalizar a requisição no mesmo instante e enviará o erro HTTP oriundo do HTTPException para o cliente.

O benefício de lançar uma exceção em vez de retornar um valor ficará mais evidente na seção sobre Dependências e Segurança.

Neste exemplo, quando o cliente pede, na requisição, por um item cujo ID não existe, a exceção com o status code 404 é lançada:

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

A response resultante

Se o cliente faz uma requisição para http://example.com/items/foo (um item_id "foo"), esse cliente receberá um HTTP status code 200, e uma resposta JSON:

{
  "item": "The Foo Wrestlers"
}

Mas se o cliente faz uma requisição para http://example.com/items/bar (ou seja, um não existente item_id "bar"), esse cliente receberá um HTTP status code 404 (o erro "não encontrado" — not found error), e uma resposta JSON:

{
  "detail": "Item not found"
}

Dica

Quando você lançar um HTTPException, você pode passar qualquer valor convertível em JSON como parâmetro de detail, e não apenas str.

Você pode passar um dict ou um list, etc. Esses tipos de dados são manipulados automaticamente pelo FastAPI e convertidos em JSON.

Adicione headers customizados

Há certas situações em que é bastante útil poder adicionar headers customizados no HTTP error. Exemplo disso seria adicionar headers customizados para tipos de segurança.

Você provavelmente não precisará utilizar esses headers diretamente no seu código.

Mas caso você precise, para um cenário mais complexo, você pode adicionar headers customizados:

from fastapi import FastAPI, HTTPException

app = FastAPI()

items = {"foo": "The Foo Wrestlers"}


@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}

Instalando manipuladores de exceções customizados

Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette

Digamos que você tenha uma exceção customizada UnicornException que você (ou uma biblioteca que você use) precise lançar (raise).

Nesse cenário, se você precisa manipular essa exceção de modo global com o FastAPI, você pode adicionar um manipulador de exceção customizada com @app.exception_handler().

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse


class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name


app = FastAPI()


@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )


@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name=name)
    return {"unicorn_name": name}

Nesse cenário, se você fizer uma requisição para /unicorns/yolo, a operação de caminho vai lançar (raise) o UnicornException.

Essa exceção será manipulada, contudo, pelo unicorn_exception_handler.

Dessa forma você receberá um erro "limpo", com o HTTP status code 418 e um JSON com o conteúdo:

{"message": "Oops! yolo did something. There goes a rainbow..."}

Detalhes Técnicos

Você também pode usar from starlette.requests import Request and from starlette.responses import JSONResponse.

FastAPI disponibiliza o mesmo starlette.responses através do fastapi.responses por conveniência ao desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette. O mesmo acontece com o Request.

Sobrescreva o manipulador padrão de exceções

FastAPI tem alguns manipuladores padrão de exceções.

Esses manipuladores são os responsáveis por retornar o JSON padrão de respostas quando você lança (raise) o HTTPException e quando a requisição tem dados invalidos.

Você pode sobrescrever esses manipuladores de exceção com os seus próprios manipuladores.

Sobrescreva exceções de validação da requisição

Quando a requisição contém dados inválidos, FastAPI internamente lança para o RequestValidationError.

Para sobrescrevê-lo, importe o RequestValidationError e use-o com o @app.exception_handler(RequestValidationError) para decorar o manipulador de exceções.

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

Se você for ao /items/foo, em vez de receber o JSON padrão com o erro:

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

você receberá a versão em texto:

1 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationError vs ValidationError

Aviso

Você pode pular estes detalhes técnicos caso eles não sejam importantes para você neste momento.

RequestValidationError é uma subclasse do ValidationError existente no Pydantic.

FastAPI faz uso dele para que você veja o erro no seu log, caso você utilize um modelo de Pydantic em response_model, e seus dados tenham erro.

Contudo, o cliente ou usuário não terão acesso a ele. Ao contrário, o cliente receberá um "Internal Server Error" com o HTTP status code 500.

E assim deve ser porque seria um bug no seu código ter o ValidationError do Pydantic na sua response, ou em qualquer outro lugar do seu código (que não na requisição do cliente).

E enquanto você conserta o bug, os clientes / usuários não deveriam ter acesso às informações internas do erro, porque, desse modo, haveria exposição de uma vulnerabilidade de segurança.

Do mesmo modo, você pode sobreescrever o HTTPException.

Por exemplo, você pode querer retornar uma response em plain text ao invés de um JSON para os seguintes erros:

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return PlainTextResponse(str(exc), status_code=400)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

Detalhes Técnicos

Você pode usar from starlette.responses import PlainTextResponse.

FastAPI disponibiliza o mesmo starlette.responses como fastapi.responses, como conveniência a você, desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette.

Use o body do RequestValidationError.

O RequestValidationError contém o body que ele recebeu de dados inválidos.

Você pode utilizá-lo enquanto desenvolve seu app para conectar o body e debugá-lo, e assim retorná-lo ao usuário, etc.

Tente enviar um item inválido como este:

{
  "title": "towel",
  "size": "XL"
}

Você receberá uma response informando-o de que a data é inválida, e contendo o body recebido:

{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

O HTTPException do FastAPI vs o HTTPException do Starlette.

O FastAPI tem o seu próprio HTTPException.

E a classe de erro HTTPException do FastAPI herda da classe de erro do HTTPException do Starlette.

A diferença entre os dois é a de que o HTTPException do FastAPI permite que você adicione headers que serão incluídos nas responses.

Esses headers são necessários/utilizados internamente pelo OAuth 2.0 e também por outras utilidades de segurança.

Portanto, você pode continuar lançando o HTTPException do FastAPI normalmente no seu código.

Porém, quando você registrar um manipulador de exceção, você deve registrá-lo através do HTTPException do Starlette.

Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar o HTTPException, o seu manipulador de exceção poderá capturar esse lançamento e tratá-lo.

from starlette.exceptions import HTTPException as StarletteHTTPException

Re-use os manipulares de exceção do FastAPI

Se você quer usar a exceção em conjunto com o mesmo manipulador de exceção default do FastAPI, você pode importar e re-usar esses manipuladores de exceção do fastapi.exception_handlers:

from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
    print(f"OMG! An HTTP error!: {repr(exc)}")
    return await http_exception_handler(request, exc)


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    print(f"OMG! The client sent invalid data!: {exc}")
    return await request_validation_exception_handler(request, exc)


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 3:
        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
    return {"item_id": item_id}

Nesse exemplo você apenas imprime (print) o erro com uma mensagem expressiva. Mesmo assim, dá para pegar a ideia. Você pode usar a exceção e então apenas re-usar o manipulador de exceção default.