# Postmortem: un pedido mal formado apaga Kallpa

Tipo: game day

Lo provocamos con `ejemplos/game-day.sh` en una computadora de desarrollo, con
una imagen de Kallpa que ya tiene `/health` y logs JSON. Las horas son de Lima.
El tráfico es de una solicitud por segundo, generado por el mismo script.

## Resumen

Una sola solicitud con la ruta `//` apagó el proceso de Kallpa, y la página dejó
de responder durante 50 segundos. El contenedor quedó detenido, y nada lo
reinició hasta que una persona corrió `docker start`. No se perdieron datos.

## Impacto

- **Indisponibilidad total:** de 01:49:08 a 01:49:58, 50 segundos.
- **Solicitudes:** fallaron 49 de 79 (`grep -c ' 000$'` sobre el registro de
  llamadas del script), además de la que causó la caída.
- **Presupuesto:** con el SLO de 99.5 % y 20 000 solicitudes al mes, el presupuesto
  es de 100; este episodio gastaría 49. Es una cuenta gruesa: en el mes real no
  llega una solicitud por segundo, y en la noche fallarían menos.
- **Datos:** ninguno perdido. La base de datos está en un volumen y las
  transacciones ya confirmadas no dependen del proceso.

## Línea de tiempo

| Hora | Qué pasó |
|---|---|
| 01:49:08 | Empieza: llega `GET //`. `new URL` lanza un error, el proceso termina con código 1 |
| 01:49:09 | Primera solicitud fallida de los vecinos |
| 01:49:18 | El monitor (`/health` cada 10 s) ve la primera falla |
| 01:49:28 | **Detección:** segunda falla seguida, el monitor avisa, 20 s después del inicio |
| 01:49:58 | Última solicitud fallida |
| 01:49:58 | **Mitigación:** quien está de turno corre `docker start` |
| 01:50:08 | Cierre: termina el ensayo con el contenedor arriba |

## Causa raíz

Inmediata: en `src/http/server.ts`, `new URL(req.url, ...)` está fuera del `try`.
La función del servidor es `async`, así que el error se vuelve una promesa
rechazada que nadie maneja, y Node termina el proceso.

Del proceso:

- **Las pruebas** no mandan rutas mal formadas al servidor: las del router tienen
  rutas válidas, y `new URL` se ejecuta antes de llegar al router.
- **El despliegue** no tiene política de reinicio: un proceso que termina se
  queda terminado. El `HEALTHCHECK` no cubre esto: solo cambia una etiqueta.
- **El monitoreo** detectó en 20 s, pero el aviso dependía de que una persona lo viera.

**Lo que salió bien:** el monitor sí mira algo que ven los usuarios, y `docker logs`
tenía el stack trace completo. **Lo que tuvo suerte:** la base de datos estaba en
un volumen; si esa misma persona hubiera recreado el contenedor con otro comando,
el problema habría sido otro.

## Acciones

| Acción | Tipo | Owner | Fecha | Estado |
|---|---|---|---|---|
| Mover `new URL` dentro del `try` y agregar una prueba que mande `GET //` y compruebe que responde un error 4xx y que el servidor sigue arriba | evita | backend | 2026-11-06 | pendiente |
| Levantar el contenedor con `--restart=on-failure` en el script de despliegue; repetir el game day y comprobar que no fallan más de 2 solicitudes | mitiga | quien despliega | 2026-11-06 | hecho: 0 de 59 fallidas, 1 reinicio |
| Avisar cuando `RestartCount` suba: un reinicio automático esconde el error si nadie lo mira | detecta | operación | 2026-11-13 | pendiente |
