Saltar al contenido

Manejo de Errores

Hay muchas situaciones en las que necesitas reportar un error a un cliente que está usando tu API.

Este cliente podría ser un navegador con un frontend, un código de otra persona, un dispositivo IoT, etc.

Podrías necesitar decirle al cliente que:

  • El cliente no tiene suficientes privilegios para esa operación.
  • El cliente no tiene acceso a ese recurso.
  • El item que el cliente intentaba acceder no existe.
  • etc.

En estos casos, normalmente retornarías un código de estado HTTP en el rango de 400 (de 400 a 499).

Esto es similar a los códigos de estado HTTP 200 (de 200 a 299). Esos códigos de estado "200" significan que de alguna manera hubo un "éxito" en la petición.

Los códigos de estado en el rango 400 significan que hubo un error por parte del cliente.

¿Recuerdas todos esos errores "404 Not Found" (y chistes)?

Usar HTTPException

Para retornar respuestas HTTP con errores al cliente usas HTTPException.

Importar 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]}

Lanzar una HTTPException en tu código

HTTPException es una excepción normal de Python con datos adicionales relevantes para APIs.

Como es una excepción de Python, no la retornas, la lanzas.

Esto también significa que si estás dentro de una función utilitaria que estás llamando dentro de tu función de operación de path, y lanzas la HTTPException desde dentro de esa función utilitaria, no ejecutará el resto del código en la función de operación de path, terminará esa petición inmediatamente y enviará el error HTTP de la HTTPException al cliente.

El beneficio de lanzar una excepción en lugar de retornar un valor será más evidente en la sección sobre Dependencias y Seguridad.

En este ejemplo, cuando el cliente solicita un item por un ID que no existe, lanza una excepción con un código de estado 404:

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]}

La respuesta resultante

Si el cliente solicita http://example.com/items/foo (un item_id "foo"), ese cliente recibirá un código de estado HTTP 200, y una respuesta JSON de:

{
  "item": "The Foo Wrestlers"
}

Pero si el cliente solicita http://example.com/items/bar (un item_id "bar" que no existe), ese cliente recibirá un código de estado HTTP 404 (el error "not found"), y una respuesta JSON de:

{
  "detail": "Item not found"
}

Consejo

Al lanzar una HTTPException, puedes pasar cualquier valor que pueda ser convertido a JSON como parámetro detail, no solo str.

Podrías pasar un dict, una list, etc.

Son manejados automáticamente por FastAPI y convertidos a JSON.

Añadir headers personalizados

Hay algunas situaciones en las que es útil poder añadir headers personalizados al error HTTP. Por ejemplo, para algunos tipos de seguridad.

Probablemente no necesites usarlo directamente en tu código.

Pero en caso de que lo necesites para un escenario avanzado, puedes añadir headers personalizados:

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]}

Instalar manejadores de excepciones personalizados

Puedes añadir manejadores de excepciones personalizados con las mismas utilidades de excepciones de Starlette.

Digamos que tienes una excepción personalizada UnicornException que tú (o una librería que usas) podrían lanzar.

Y quieres manejar esta excepción globalmente con FastAPI.

Podrías añadir un manejador de excepciones personalizado con @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}

Aquí, si solicitas /unicorns/yolo, la operación de path lanzará una UnicornException.

Pero será manejada por el unicorn_exception_handler.

Así que recibirás un error limpio, con un código de estado HTTP 418 y un contenido JSON de:

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

Detalles Técnicos

También podrías usar from starlette.requests import Request y from starlette.responses import JSONResponse.

FastAPI proporciona las mismas starlette.responses como fastapi.responses solo como una comodidad para ti, el desarrollador. Pero la mayoría de las respuestas disponibles vienen directamente de Starlette. Lo mismo con Request.

Sobrescribir los manejadores de excepciones por defecto

FastAPI tiene algunos manejadores de excepciones por defecto.

Estos manejadores se encargan de retornar las respuestas JSON por defecto cuando lanzas una HTTPException y cuando la petición tiene datos inválidos.

Puedes sobrescribir estos manejadores de excepciones con los tuyos.

Sobrescribir las excepciones de validación de peticiones

Cuando una petición contiene datos inválidos, FastAPI internamente lanza un RequestValidationError.

Y también incluye un manejador de excepciones por defecto para ello.

Para sobrescribirlo, importa el RequestValidationError y úsalo con @app.exception_handler(RequestValidationError) para decorar el manejador de excepciones.

El manejador de excepciones recibirá una Request y la excepción.

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: RequestValidationError):
    message = "Validation errors:"
    for error in exc.errors():
        message += f"\nField: {error['loc']}, Error: {error['msg']}"
    return PlainTextResponse(message, 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}

Ahora, si vas a /items/foo, en lugar de obtener el error JSON por defecto con:

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

obtendrás una versión en texto, con:

Validation errors:
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer

Sobrescribir el manejador de errores de HTTPException

De la misma manera, puedes sobrescribir el manejador de HTTPException.

Por ejemplo, podrías querer retornar una respuesta en texto plano en lugar de JSON para estos errores:

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: RequestValidationError):
    message = "Validation errors:"
    for error in exc.errors():
        message += f"\nField: {error['loc']}, Error: {error['msg']}"
    return PlainTextResponse(message, 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}

Detalles Técnicos

También podrías usar from starlette.responses import PlainTextResponse.

FastAPI proporciona las mismas starlette.responses que fastapi.responses solo como una conveniencia para ti, el desarrollador. Pero la mayoría de las respuestas disponibles vienen directamente de Starlette.

Aviso

Ten en mente que el RequestValidationError contiene la información del nombre del archivo y la línea donde ocurre el error de validación para que puedas mostrarlo en tus logs con la información relevante si quieres.

Pero eso significa que si simplemente lo conviertes a un string y retornas esa información directamente, podrías estar filtrando un poco de información sobre tu sistema, por eso aquí el código extrae y muestra cada error de manera independiente.

Usar el body de RequestValidationError

El RequestValidationError contiene el body que recibió con datos inválidos.

Podrías usarlo mientras desarrollas tu app para loguear el body y debuguearlo, retornarlo al usuario, etc.

from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )


class Item(BaseModel):
    title: str
    size: int


@app.post("/items/")
async def create_item(item: Item):
    return item

Ahora intenta enviar un item inválido como:

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

Recibirás una respuesta diciéndote que los datos son inválidos conteniendo el body recibido:

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

HTTPException de FastAPI vs HTTPException de Starlette

FastAPI tiene su propio HTTPException.

Y la clase de error HTTPException de FastAPI hereda de la clase de error HTTPException de Starlette.

La única diferencia es que el HTTPException de FastAPI acepta cualquier dato convertible a JSON para el campo detail, mientras que el HTTPException de Starlette solo acepta strings para ello.

Así que puedes seguir lanzando el HTTPException de FastAPI como normalmente en tu código.

Pero cuando registras un manejador de excepciones, deberías registrarlo para el HTTPException de Starlette.

De esta manera, si cualquier parte del código interno de Starlette, o una extensión o plug-in de Starlette, lanza un HTTPException de Starlette, tu manejador podrá capturarlo y manejarlo.

En este ejemplo, para poder tener ambos HTTPException en el mismo código, la excepción de Starlette se renombra a StarletteHTTPException:

from starlette.exceptions import HTTPException as StarletteHTTPException

Reutilizar los manejadores de excepciones de FastAPI

Si quieres usar la excepción junto con los mismos manejadores de excepciones por defecto de FastAPI, puedes importar y reutilizar los manejadores de excepciones por defecto de 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}

En este ejemplo solo estás imprimiendo el error con un mensaje muy expresivo, pero te haces una idea. Puedes usar la excepción y luego simplemente reutilizar los manejadores de excepciones por defecto.