RUTAS DINÁMICAS Y EXCEPCIONES EN FLASK¶
Tipos en los parámetros de URL¶
Recordatorio: parámetros variables¶
Una URL puede contener partes que cambian: el código de un producto, una cantidad, un identificador. En los apuntes anteriores vimos que Flask captura esas partes con la notación <nombre>:
Por defecto, lo que captura <codigo> es siempre texto: si pides /producto/A1, codigo vale "A1"; si pides /producto/12, codigo vale "12" (cadena, no número). Esto es suficiente cuando la URL lleva un código o un nombre, pero no cuando esperas un número para operar con él.
Converters de Flask¶
Un converter es un validador de tipo que Flask aplica al segmento de URL antes de llamar a tu función. Se escribe con la sintaxis <tipo:nombre>. Si el valor no cumple el tipo, Flask responde con un 404 automáticamente y tu función ni siquiera se ejecuta.
Flask ofrece cinco converters integrados:
| Converter | Qué acepta | Tipo Python | Uso típico |
|---|---|---|---|
<string:x> |
Texto sin / (converter por defecto) |
str |
Códigos, nombres, slugs |
<int:x> |
Números enteros sin signo | int |
Cantidades, identificadores |
<float:x> |
Números decimales con punto | float |
Precios, porcentajes |
<path:x> |
Texto que puede incluir / |
str |
Rutas de ficheros anidadas |
<uuid:x> |
Identificadores UUID | UUID |
Identificadores únicos |
Cuando se omite el tipo (
<codigo>), Flask aplicastringpor defecto.
Las regex detrás de los converters¶
Cada converter tiene por dentro una expresión regular (regex) — una fórmula que describe qué forma tiene que tener el texto para ser aceptado. Si no conoces las expresiones regulares, basta con saber esto: son patrones que describen qué caracteres están permitidos y en qué orden.
| Converter | Regex interna | Qué significa |
|---|---|---|
<int:x> |
\d+ |
Uno o más dígitos (0-9). No acepta signo ni decimales. |
<float:x> |
\d+\.\d+ |
Dígitos, punto, dígitos. No acepta enteros puros ni signo. |
<string:x> |
[^/]+ |
Cualquier carácter salvo /. |
Esto tiene consecuencias sorprendentes cuando el usuario teclea la URL:
| URL solicitada | Resultado | Por qué |
|---|---|---|
/comprar/3 |
Llega al route con cantidad=3 |
Matchea \d+ |
/comprar/tres |
404 | tres no son dígitos |
/comprar/-1 |
404 | El signo - no es un dígito |
/insertar/1.5 |
Llega con cantidad=1.5 |
Matchea \d+\.\d+ |
/insertar/1 |
404 | <float> exige punto decimal |
/insertar/-1.5 |
404 | El signo - tampoco es un dígito |
Flask filtra antes que tu código
Si el converter rechaza el valor, tu función de vista no se llega a ejecutar. Flask devuelve un 404 directamente. Esto significa que los try/except ValueError que pongas dentro de la función nunca se disparan para valores inválidos de tipo — solo se disparan si el dominio rechaza el valor una vez convertido.
Dónde termina Flask y empieza el dominio¶
Los converters solo validan el formato. La validación de negocio (que el precio sea positivo, que la cantidad sea razonable, que el código exista) sigue en el dominio. Flask filtra lo que no cumple el tipo; el dominio filtra lo que no cumple las reglas.
En este ejemplo,
cantidad=0.0sí llega al dominio (matchea\d+\.\d+). Eltry/exceptcaptura elValueErrorque lanzainsertar_dineroporque la cantidad no es positiva. En cambio,cantidad=-1.5nunca llega — Flask responde 404 antes.
Mala práctica: validar el tipo en el dominio
## Incorrecto: el dominio no debe preocuparse del formato de la URL
@app.route("/insertar/<cantidad>") ## string por defecto
def insertar(cantidad):
try:
cantidad_num = float(cantidad) ## conversión manual
servicio.insertar_dinero(cantidad_num)
except ValueError:
return "Formato incorrecto o cantidad inválida", 400
Códigos HTTP como lenguaje común¶
Qué es un código HTTP¶
Cada respuesta HTTP empieza con un código de estado: un número de tres dígitos que resume qué ha pasado. El navegador lo lee antes que el cuerpo para saber si la petición ha ido bien, si hay que redirigir, si hay un error del cliente o del servidor.
HTTP/1.1 200 OK ← código de estado + descripción
Content-Type: text/html
<h1>Productos</h1> ← cuerpo
Los códigos están agrupados por la primera cifra:
| Rango | Familia | Significado |
|---|---|---|
2xx |
Éxito | La petición ha ido bien. |
3xx |
Redirección | El recurso está en otra URL; el navegador debe ir ahí. |
4xx |
Error del cliente | La petición tiene algo mal: URL, datos, permisos. |
5xx |
Error del servidor | El servidor ha fallado al procesar una petición correcta. |
Los códigos que usarás en este lab¶
No hace falta memorizar todos los códigos. Estos son los relevantes para una app como la expendedora:
| Código | Nombre | Cuándo lo devolvemos |
|---|---|---|
200 |
OK | Respuesta normal con contenido. |
302 |
Found (redirección) | Tras una acción, rediriges a otra URL. |
400 |
Bad Request | El cliente mandó datos inválidos (cantidad negativa, precio cero…). |
404 |
Not Found | El recurso pedido no existe (producto con código inexistente, URL no registrada). |
409 |
Conflict | La petición choca con el estado actual (alta con un código que ya existe). |
500 |
Internal Server Error | El servidor ha lanzado una excepción no controlada. |
Devolver un código desde un route¶
Por defecto, cuando una función de vista hace return "texto", Flask añade el código 200 OK. Para devolver otro código, return acepta una tupla (cuerpo, codigo):
El código va como segundo elemento de la tupla, no como argumento con nombre. La descripción (
"Not Found") la añade Flask por ti a partir del número.
Elegir el código correcto¶
La elección del código no es arbitraria. Herramientas automáticas (navegadores, motores de búsqueda, clientes de API) actúan según el código recibido:
- El navegador solo cachea respuestas
200. - Un
302hace que el navegador cargue automáticamente la URL indicada en la cabecera. - Un
404le dice al motor de búsqueda "este enlace está roto, no lo indexes". - Un
500indica un fallo del servidor: los sistemas de monitorización lo cuentan aparte de los4xx.
| Situación | Código | Por qué no otro |
|---|---|---|
| Producto con código inexistente | 404 |
El recurso no existe. No es culpa de los datos del cliente: la URL apuntaba a algo que no está. |
| Precio de alta con valor cero | 400 |
Los datos enviados son inválidos; el cliente debe reenviar con otros distintos. |
| Alta con código ya existente | 409 |
La petición está bien formada, pero choca con el estado del servidor. Un 400 ocultaría que el problema es el estado, no los datos. |
| Excepción no capturada en un route | 500 |
Es un fallo del servidor que no habíamos previsto. |
Elige el código que comunique mejor
Si dudas entre 400 y 404, piensa: ¿el cliente mandó datos malos (400) o pidió algo que no está (404)? Si dudas entre 400 y 409, piensa: ¿el problema está en los datos (400) o en el estado del servidor (409)?
Redirección y URLs declarativas¶
El código 302 y redirect¶
Un redirect (redirección) es una respuesta HTTP que le dice al navegador: "no estoy en esta URL, ve a esta otra". El código habitual es 302 Found. El navegador, al recibirla, carga automáticamente la URL indicada sin que el usuario haga nada.
Flask tiene una función auxiliar redirect(url) que construye esa respuesta:
El problema de los strings hardcodeados¶
Escribir la URL a mano (como "/productos" arriba) funciona, pero se rompe en cuanto cambiamos la ruta. Imagina que decidimos renombrar /productos a /catalogo: tendríamos que buscar y sustituir cada redirect("/productos") repartido por el código. Es error-prono y fácil de olvidar.
url_for y el desacoplamiento¶
url_for() construye una URL a partir del nombre de la función de vista, no del string de la URL. Si cambias la ruta del decorador, url_for sigue devolviendo la URL correcta.
Para rutas con parámetros, se pasan como argumentos con nombre:
El primer argumento de
url_fores el nombre de la función Python, no la URL. Esto desacopla el código de las URLs concretas: si mañana cambias@app.route("/producto/<codigo>")por@app.route("/item/<codigo>"), todas las llamadas aurl_for("ver_producto", …)siguen funcionando.
El patrón "actúa → redirige"¶
Cuando un route modifica el estado (comprar, agregar, eliminar), conviene redirigir al acabar en lugar de devolver directamente el mensaje de éxito. Así evitas un problema clásico: si el usuario pulsa recargar en el navegador, la última petición se reenvía, y si esa petición era una acción destructiva (comprar, eliminar) se repite sin querer.
Tras el
redirect, el navegador queda en/producto/<codigo>. Si el usuario pulsa recargar, lo que se repite es la consulta al detalle, no el alta. Es la consulta la que se vuelve a ejecutar, no la acción.
Mala práctica: devolver el resultado de la acción como página final
Estado de la aplicación en el servidor¶
El servicio global¶
En la expendedora, creamos un único objeto servicio al arrancar app.py y lo usamos desde todos los routes:
Ese servicio vive en memoria mientras el proceso Python esté corriendo. Es la misma instancia que atiende a todas las peticiones. Su estado interno (_saldo, _seleccion) se acumula entre peticiones.
Máquina de estados en el servidor¶
La máquina expendedora es una máquina de estados: no puedes comprar sin antes seleccionar un producto e insertar dinero. El orden importa.
Al exponer esto en Flask, cada route es una transición de estado, no una transacción completa. El flujo de compra queda repartido en varias peticiones HTTP:
GET /seleccionar/A1 → _seleccion = Item(A1)
GET /insertar/2.0 → _saldo += 2.0
GET /comprar → compra con _seleccion y _saldo, reinicia
Entre una petición y la siguiente pueden pasar segundos o minutos. El navegador no guarda el estado; lo guarda el servidor dentro del objeto
servicio.
Problema 1: estado compartido entre usuarios¶
Como servicio es uno solo, si dos navegadores apuntan al mismo servidor a la vez, se pisan el estado. El usuario A hace /seleccionar/A1, y antes de que pulse /comprar el usuario B hace /seleccionar/B2 — la selección queda como B2 para todos. Cuando A pulse /comprar, comprará B2, no lo que pidió.
Esto no es un fallo del dominio: el dominio está diseñado para un usuario. El fallo es asumir que ese único estado global puede servir a cualquier cantidad de clientes.
Problema 2: estado perdido al reiniciar¶
El estado vive en la RAM del proceso. Si detienes el servidor con Ctrl+C y lo vuelves a arrancar, el _saldo y la _seleccion vuelven a cero. Lo único que sobrevive es lo que hay en disco: la base de datos SQLite con los productos.
Hacia dónde vamos¶
Estos dos problemas no se resuelven en este lab; requieren herramientas que aún no hemos visto:
- Persistencia del estado en fichero JSON. Si guardamos
_saldoy_seleccionen un fichero tras cada operación, sobreviven al reinicio. Conecta con los apuntes de UT3 sobre ficheros y con la serialización JSON. - Sesiones HTTP (
flask.session). Cada navegador obtiene una "cookie" que identifica su propio estado en el servidor. Resuelve el problema de usuarios concurrentes. Lo veremos cuando introduzcamos formularios.
Por ahora basta con reconocer las limitaciones: la expendedora web, tal como está, es monousuario y volátil. Es honesto nombrarlo.
Parámetros en URL vs formularios HTML¶
Los límites de la URL como entrada de datos¶
Hasta ahora, todas las entradas del usuario viajan en la URL: /agregar/X1/Zumo/1.5/10. Esto funciona para probar routes, pero tiene problemas serios para un usuario real.
Legibilidad¶
Una URL como /agregar_descuento/X1/Zumo/1.5/10/20.0 es ilegible. El orden de los parámetros es arbitrario y no se indica qué significa cada valor. Un error de orden (poner el porcentaje donde va la cantidad) se acepta sin avisar.
Caracteres especiales¶
Los nombres con espacios, acentos o barras no se pueden poner tal cual en una URL: "Zumo de naranja" tiene que pasarse como "Zumo%20de%20naranja" (URL-encoding). El usuario no va a escribir eso a mano.
El problema del separador decimal¶
Aquí entra un concepto del sistema operativo llamado locale. El locale es el conjunto de convenciones culturales que un sistema aplica: idioma de los mensajes, formato de las fechas, separador decimal, símbolo de moneda. Los locales se nombran con códigos como es_ES (español de España), en_US (inglés de EE. UU.) o fr_FR (francés de Francia).
El detalle que nos afecta: en es_ES el separador decimal es la coma (1,5), mientras que en en_US y en la mayoría de lenguajes de programación es el punto (1.5). Flask —como todo Python— espera siempre el punto porque es la convención del código, independientemente del idioma del sistema operativo.
Resultado: un usuario español que teclee /agregar/X1/Zumo/1,5/10 obtendrá un 404 misterioso. Desde su punto de vista, "1,5" es un número válido; desde el punto de vista del converter de Flask, no matchea \d+\.\d+.
Hacia los formularios HTML¶
En un próximo lab pasaremos a recoger los datos con formularios HTML (<form>). Cada campo del formulario tiene su propia casilla etiquetada, el navegador se encarga del encoding y los datos viajan en el cuerpo de la petición, no en la URL. Además, los formularios permiten validación en el cliente y una experiencia de usuario mucho más natural.
| Aspecto | Parámetros en URL | Formularios HTML |
|---|---|---|
| Legibilidad | Baja: orden y separadores frágiles | Alta: cada campo etiquetado |
| Caracteres especiales | Hay que codificar manualmente | Gestionado por el navegador |
| Separador decimal | Obligatorio . |
El <input type="number"> abstrae el formato |
| Aptos para producción | Solo para pruebas y lecturas simples | Sí |
Por eso usar parámetros en la URL para lecturas (
/producto/A1) es razonable, pero usarlos para altas (/agregar/X1/Zumo/1.5/10) es una solución provisional que reemplazaremos en el siguiente lab.
Mala práctica: rutas con decenas de parámetros en la URL
La URL identifica el recurso; los datos van en el cuerpo del formulario.