Stack: DRF · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 13 — REST bien hecho
Objetivos
- Implementar
Idempotency-Keyde verdad: guardar respuesta, reenviar la misma, expirar claves. - Diseñar endpoints idempotentes por naturaleza (PUT, DELETE) y proteger los que no pueden serlo (POST de pago).
- Evolucionar la API sin romper clientes (compatibilidad hacia atrás y versionado).
1. El problema: el reintento que cobra dos veces
El cliente envía POST /reservations/{id}/payments/, timeout de red, reintenta — y el servidor ya procesó el primero: dos pagos. Ni tu código ni la cola lo evitan: el cliente reintenta legítimamente (las redes fallan). La solución es el contrato de idempotencia:
POST /api/reservations/{uuid}/payments/
Idempotency-Key: 7f3c9a2e-... ← genera el CLIENTE, única por intenciónServidor:
- La clave no existía → procesa, guarda la respuesta asociada a la clave (con su status code).
- La clave existe con el mismo cuerpo del request → devuelve la respuesta guardada (sin reprocesar).
- La clave existe con otro cuerpo →
422 Unprocessable(uso incorrecto: la clave es por intención). - Claves viejas se expiran (TTL 24h) — son cache de intenciones, no histórico para siempre.
Almacenamiento: tabla idempotency_key(key PK, request_hash, response_status, response_body, created_at) — transaccional con el proceso (la 10): la clave y el efecto se graban en el mismo commit.
2. Idempotencia por diseño (gratis, sin tabla)
| Operación | ¿Idempotente? | Por qué |
|---|---|---|
PUT /events/{uuid} (reemplazo completo) | sí | el mismo PUT dos veces deja el mismo estado |
DELETE /reservations/{uuid} (cancelar) | sí si lo haces no-op al repetir | el cancel() de la 00b ya devolvía False |
GET | sí | no muta |
POST /reservations/ | NO por diseño (crea recurso nuevo) | la intent-key lo protege |
POST /payments/ | NO (cobra) | la intent-key es OBLIGATORIA |
La I3 del proyecto ("solo se paga una vez") se cumple por dos capas: idempotency_key UNIQUE en Payment (BD) + el mecanismo de esta lección (API). Defense in depth de nuevo.
3. Compatibilidad hacia atrás: qué se puede cambiar sin romper
Seguro (clientes viejos siguen vivos): añadir campos a respuestas; añadir endpoints; añadir parámetros opcionales con default; añadir valores nuevos que el cliente viejo ignora.
Rompedor (requiere versión): quitar o renombrar campos; cambiar tipos (string→int); cambiar semántica de un código de error; hacer obligatorio un parámetro; cambiar el formato de paginación.
Regla operativa: los clientes no actualizan cuando tú quieres (apps móviles instaladas, scripts de integradores). Cada cambio rompedor = nueva versión mayor conviviendo:
/api/v1/events/ (congelada, bugfixes de seguridad)
/api/v2/events/ (viva)Versionado en la ruta (no en header: se comparte y depura mejor). Deprecación anunciada: cabecera Deprecation + fecha de corte documentada, y métrica de uso por versión antes de apagar v1 (Lección 46).
4. Idempotencia en el webhook (el gemelo)
La pasarela reintenta sus webhooks (at-least-once, Lección 30): el mismo evento de pago puede llegar 3 veces. Tu handler: verificar firma → buscar payment.idempotency_key = event_id de la pasarela → si existe, responder 200 y no hacer nada (el reintento exitoso es esperado, no error). Esta simetría cliente/pasarela es la razón por la que I3 existe.
Autoevaluación
- ¿Por qué la clave de idempotencia la genera el CLIENTE y no el servidor?
- ¿Qué guarda el servidor exactamente para responder idempotente y por qué el request_hash es obligatorio?
- ¿Por qué la tabla de claves vive en la misma transacción que el efecto (la 10)? ¿Qué bug produce separarlas?
- Enumera 3 cambios "seguros" y 3 "rompedores" sobre tu API actual.
- ¿Cómo apagarías v1 con datos en la mano y sin romper a nadie de golpe?
Continúa con los ejercicios. Las solutions.md solo tras intentarlo.