Saltar a contenido

MANEJADORES DE ERROR, INTROSPECCIÓN Y LOGGING EN FLASK

Estos apuntes son la segunda parte de la sesión 2 de UT4. Asumen que ya has leído poo_ut4_2_rutas_dinamicas_excepciones.md (tipos en URL, códigos HTTP, redirect, estado en el servidor, parámetros URL vs formularios).

Manejadores globales de error

El problema que resuelven

Hasta ahora, cada route captura sus propias excepciones de dominio con try/except. Pero hay errores que no ocurren dentro de ningún route: URLs que no coinciden con ninguna ruta registrada, divisiones por cero accidentales, variables no definidas o ficheros que no se pueden abrir.

Sin manejo, Flask muestra una página genérica (o, en modo debug, el traceback completo). Con un manejador de error podemos controlar qué ve el usuario en esos casos.

El decorador @app.errorhandler

Un manejador de error es una función registrada con @app.errorhandler(codigo) que Flask ejecuta cuando se genera un error con ese código. Funciona como un route especial que captura por código HTTP en vez de por URL (URL = /producto/A1; código HTTP = 404).

1
2
3
4
5
6
7
@app.errorhandler(404)
def no_encontrado(e):
    return f"<h2>404 — No encontrado</h2><p>{e}</p>", 404

@app.errorhandler(500)
def error_servidor(e):
    return "<h2>500 — Error del servidor</h2>", 500

El parámetro e recibe el objeto de excepción que Flask ha generado. Podemos interpolarlo en el mensaje, loguearlo o ignorarlo según convenga.

¿Qué excepción recibimos en e?

Internamente Flask lanza werkzeug.exceptions.NotFound cuando ninguna URL coincide, InternalServerError cuando una excepción se escapa de un route, etc. No tenemos que importarlas ni instanciarlas: el manejador las recibe ya construidas en el parámetro e.

Dónde colocar los manejadores en app.py

Coloca los manejadores junto al resto de routes. Suelen ir al final del fichero, separados con un comentario tipo # --- Manejadores globales ---, para que se vean de un golpe y no se confundan con los routes normales.

Interacción con los try/except de los routes

Los manejadores globales no sustituyen al try/except dentro de los routes: lo complementan. Cada uno captura cosas distintas:

Situación ¿Quién responde?
URL no registrada (p.ej. /foo) @app.errorhandler(404) global
Producto no encontrado capturado en route try/except del route (devuelve str(e), 404)
Excepción no capturada en un route @app.errorhandler(500) global
Converter de tipo que rechaza el valor (p.ej. <int:cantidad> con tres; ver apuntes UT4-2) @app.errorhandler(404) global

Sin manejador 404, una URL inexistente muestra una página técnica de Flask en inglés. Con manejador, ves tu página personalizada y coherente con el resto de la app. Los manejadores globales son una red de seguridad: atrapan lo que se escapa de los try/except, no lo que ya está bien manejado.

Manejadores globales para experiencia de usuario coherente

Sin manejadores globales, un 404 por URL inexistente muestra una página Flask genérica, mientras que un 404 por producto inexistente muestra tu mensaje. El usuario ve dos experiencias distintas para el mismo código de error. Con manejadores globales unificas la apariencia.


Introspección: la app conoce sus rutas

Qué es la introspección

Introspección es la capacidad de un programa de observar su propio estado en tiempo de ejecución. Flask permite consultar qué rutas tiene registradas sin leer el código fuente: la información está viva dentro del objeto app.

Ya has visto introspección antes en Python: dir(objeto) te dice qué métodos tiene un objeto, y type(x) te dice de qué clase es. Aquí hacemos lo mismo con la app: le preguntamos qué rutas tiene.

El objeto app.url_map

app.url_map es el mapa de rutas que Flask usa internamente para decidir qué función de vista llamar ante una URL. Cada vez que escribimos @app.route("/algo"), Flask añade esa ruta a app.url_map por dentro. Por eso no tenemos que crear el mapa: ya está construido cuando arranca la app. Desde fuera, lo podemos iterar con iter_rules():

1
2
3
4
5
6
7
8
9
@app.route("/ayuda")
def ayuda():
    lineas = ["<h2>Rutas disponibles</h2><ul>"]
    for regla in app.url_map.iter_rules():
        if regla.endpoint != "static":
            # regla.rule es el patrón "/producto/<codigo>", no una URL concreta como "/producto/A1"
            lineas.append(f"<li><code>{regla.rule}</code> — {regla.endpoint}</li>")
    lineas.append("</ul>")
    return "\n".join(lineas)

Cada regla tiene dos atributos importantes:

Atributo Qué contiene Ejemplo
regla.rule El patrón de la URL tal como se declaró /producto/<codigo>
regla.endpoint El nombre de la función de vista ver_producto

Fíjate: regla.rule es el patrón, no una URL concreta. Muestra <codigo> literal, no A1 o B2. Para generar URLs concretas necesitarías url_for con valores.

Patrón lista + append + join

El patrón lineas = [...]; lineas.append(x); "\n".join(lineas) es la forma habitual en Python de construir un texto largo a partir de muchas piezas. Es preferible a concatenar con + en un bucle: más rápido y más legible. Lo verás muchas veces a lo largo del curso.

La regla static

Flask registra automáticamente una ruta /static/<path:filename> para servir ficheros estáticos (CSS, imágenes, JavaScript). Conviene filtrarla en páginas de ayuda porque no forma parte de tu lógica de aplicación.

<path:filename> usa el converter path, otro converter de Flask que acepta cualquier cosa hasta el final de la URL, incluyendo barras (a diferencia de <int:> o <float:> que vimos en los apuntes anteriores). No tendremos que escribirlo en este lab — solo aparece aquí para que entendamos qué estamos filtrando con regla.endpoint != "static":

1
2
3
for regla in app.url_map.iter_rules():
    if regla.endpoint != "static":
        ...

Usos típicos

Caso de uso Qué hacemos
Página /ayuda Listar todas las rutas para el usuario.
Documentación automática Generar un esquema de la API en JSON.
Depuración Comprobar que un route se registró (útil cuando 404 aparece y no sabes por qué).

Una página de ayuda siempre al día

Generar /ayuda por introspección garantiza que nunca queda desactualizada. Añades un route nuevo y automáticamente aparece.


Hooks y logging

Qué es un hook

Un hook (literalmente gancho) es un punto del flujo de un programa en el que puedes "enganchar" código propio para que se ejecute automáticamente, sin modificar el flujo original. Flask ofrece varios hooks:

Hook Cuándo se ejecuta
@app.before_request Antes de cada petición, pase por el route que pase.
@app.after_request Después de cada petición que ha terminado bien.
@app.teardown_request Al final de cada petición, haya ido bien o mal (sirve para liberar recursos).

En este apuntes solo usaremos before_request. Los hooks permiten añadir comportamientos transversales (logging, autenticación, medición de tiempos) sin tocar los routes uno por uno.

El módulo logging

logging es el módulo estándar de Python para registrar mensajes durante la ejecución.

Imagina que cierras la terminal y mañana alguien te pregunta "¿qué hizo tu app ayer a las 17h?". Con print no queda nada — los mensajes se fueron con la consola. Con logging queda escrito en un fichero, con timestamp, y puedes filtrar por nivel de gravedad.

A diferencia de print, permite:

  • Escribir a un fichero en vez de a la consola.
  • Clasificar mensajes por nivel (DEBUG, INFO, WARNING, ERROR, CRITICAL).
  • Añadir timestamp y contexto automáticamente.

Configuración mínima:

1
2
3
4
5
6
7
import logging

logging.basicConfig(
    filename="expendedora.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)

Dónde colocar basicConfig en app.py

Esta configuración se pone una sola vez, al inicio del fichero, justo después de los import y antes de instanciar app = Flask(__name__). Si la ponemos después del primer app.logger.info(...) no tendrá efecto sobre las llamadas anteriores.

La sintaxis %(nombre)s del parámetro format es la del módulo loggingno es f-string ni .format(). Para este lab basta con copiar el formato; no tenemos que dominarla.

Parámetro Qué hace
filename Fichero donde se escriben los mensajes. Si no existe, se crea.
level Listón mínimo. Si ponemos INFO, registra INFO/WARNING/ERROR/CRITICAL y descarta DEBUG. Como un termómetro: lo que está por encima del listón pasa; lo de abajo no.
format Plantilla del mensaje. %(asctime)s es el timestamp, %(levelname)s el nivel, %(message)s el texto.

@app.before_request y el objeto request

El hook @app.before_request decora una función que Flask llama antes de cualquier route. Dentro de ella tienes acceso a request, un objeto global que describe la petición actual:

1
2
3
4
5
from flask import request

@app.before_request
def log_peticion():
    app.logger.info(f"{request.method} {request.path}")

Propiedades más útiles de request (ahora solo conviene conocer estas dos):

Propiedad Qué contiene Ejemplo
request.method Método HTTP "GET"
request.path Ruta pedida (sin el dominio) "/producto/A1"

app.logger vs logging.info directo

logging.info(...) y app.logger.info(...) son equivalentes para nuestro caso, pero app.logger es la forma idiomática en Flask: usa la configuración de la app y permite distinguir tus logs de los de Flask cuando depures. Tómalo como costumbre desde el principio.

Dónde colocar el hook en app.py

El hook se registra junto al resto de configuración global, al principio del fichero, después de la creación de app. El orden de declaración no importa: Flask los descubre todos al cargar el módulo, no al ejecutar línea por línea.

Cómo verificar que el logging funciona

Tras configurarlo, arranca el servidor (python -m expendedora.presentation.app), visita varias URLs en el navegador y comprueba:

  1. En la carpeta desde la que arrancaste la app aparece el fichero expendedora.log.
  2. Cada petición ha generado una línea con timestamp, nivel y método/ruta.

Si no aparece el fichero: revisa que basicConfig esté antes de cualquier app.logger.info(...). Si aparece pero no se rellena: revisa que el level no sea más alto que el nivel del mensaje (level=logging.INFO registra info(); level=logging.WARNING lo descarta).

Qué se versiona y qué no

Los ficheros de log son generados en ejecución: cambian cada vez que corres la app, dependen de qué URLs visites, contienen información de depuración. No se versionan en Git: se añaden al .gitignore. El código que genera el log sí se versiona; el log en sí no.

## .gitignore
expendedora.log
*.log

Mala práctica: usar print como sistema de registro

## Incorrecto: no queda traza tras cerrar la terminal, no se puede filtrar
@app.before_request
def log_peticion():
    print(f"{request.method} {request.path}")
## Correcto
@app.before_request
def log_peticion():
    app.logger.info(f"{request.method} {request.path}")