Cómo capturar las excepciones no controladas con FastAPI
¡Muy buenas! ¿Alguna vez se te ha escapado una excepción en uno de tus proyectos? ¿Esa excepción ha mostrado información delicada como por ejemplo información de la base datos?.
Pues bien, no sufras más porque tiene solución 😎. En el tutorial de esta semana vamos a aprender como controlar las excepciones no capturadas con FastAPI, mostrar al usuario un mensaje que no lo lleve la confusión y así hacer más robustas nuestras aplicaciones.
Antes de seguir y si quieres aprender más sobre las excepciones con Python, te dejo este tutorial en el que aprenderás todo lo que necesitas saber sobre ellas 👇.
Y dicho esto vamos al tutorial. Para ver este ejemplo, me voy a crear un proyecto muy sencillo que solo necesitará la instalación de FastAPI y uvicorn. Como siempre voy a crear un entorno virtual y lo voy a activar:
python -m venv env
# Activación windows
env/Scrips/activate
# Activación en mac/linux
source env/bin/activate
Una vez creado, instalamos FastAPI y uvicorn:
pip install fastapi uvicorn
Ahora que ya tenemos el entorno preparado y las librerías instaladas, he creado el archivo main.py con el siguiente contenido:
from fastapi import FastAPI
app = FastAPI()
@app.get('/check', tags=["Check status"])
def home():
return {"message": "API it's alive"}
Como podéis observar, es un servicio bastante sencillo con una única ruta y que no tiene pinta de que se vaya a romper por ningún sitio así que vamos a forzar una excepción que añadiremos en la función home:
@app.get('/check', tags=["Check status"])
def home():
raise Exception('Vamos a romper el proyecto')
return {"message": "API it's alive"}
Si ahora levantamos el servicio (con el comando: uvicorn main:app --reload), vamos al navegador y nos dirigimos a la ruta http://localhost:8000/check, nos aparecerá el mensaje de error "Internal Server Error" en texto plano. Aquí por lo menos FastAPI ya controla que no aparezca el mensaje real de la excepción, pero seguimos sin saber que ha pasado y no le estamos devolviendo una estructura clara. Si por ejemplo estamos construyendo una API que retorna la información en formato JSON, lo suyo es que devolvamos todos los mensajes de error en ese mismo formato ¿No?.
Para solucionar este problema vamos a cambiar un poco nuestro archivo main.py de forma que ahora quedará así:
from fastapi import FastAPI
from fastapi import Request, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(Exception)
async def catch_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "An error has occurred, please try later"},
)
@app.get('/check', tags=["Check status"])
def home():
raise Exception('Vamos a romper el proyecto')
return {"message": "API it's alive"}
Como podéis observar, hemos añadido una nueva función que tiene el decorador exception_handler. Este se encargará de capturar todas las excepciones que no tengamos controladas.
Para darle una respuesta al usuario, utilizamos la clase JSONResponse de FastAPI en la que podemos indicar el estado http que queremos devolver y una respuesta dentro de content que FastAPI se encargará de convertirla en una respuesta en formato JSON.
Para mantener el formato de errores de FastAPI, vamos a retornar el mensaje de error dentro de una clave llamada detail.
Esto ya tiene muy buena pinta, pero nos falta algo. Ya tenemos controlados los errores, pero si no tenemos una forma de saber que ha ocurrido este error nunca podremos corregirlo.
Para solucionar esto tenemos varias opciones. Cada vez que entremos en esta función, podemos enviarnos un email, un mensaje por slack (que por cierto os dejo este tutorial si queréis aprender a crear un slack bot Cómo crear un Slack bot en Python para automatizar el envío de mensajes a un canal), también podemos enviar avisos por Telegram o utilizar el paquete logging de Python para guardar los errores en un archivo de log como vamos a hacer en este ejemplo.
Volvemos al archivo main.py y reemplazamos el código para que ahora quede así:
from fastapi import FastAPI
from fastapi import Request, status
from fastapi.responses import JSONResponse
import logging
logging.basicConfig(filename='./logs/api.log', encoding='utf-8', level=logging.DEBUG)
app = FastAPI()
@app.exception_handler(Exception)
async def catch_exception_handler(request: Request, exc: Exception):
logging.error(exc)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "An error has occurred, please try later"},
)
@app.get('/check', tags=["Check status"])
def home():
raise Exception('Vamos a romper el proyecto')
return {"message": "API it's alive"}
Como veis, primero importo el paquete logging que ya viene incluido con Python y lo configuro para guardar todos los logs dentro de un archivo llamado api.log y este se guardará en la carpeta logs (deberéis generar esta carpeta antes para evitar problemas).
Después en nuestra función catch_exception_handler, justo antes de retornar la respuesta, guardamos la excepción en el log. Posteriormente, solo deberíamos revisar nuestro log periódicamente para comprobar si hay errores.
Si tenéis algún problema con el proyecto, os dejo el enlace al repositorio y recordad que podéis dejar vuestras dudas en la caja de comentarios.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.