Ejercicio 1 — El cobro doble
- Si generas
idempotency_keyconuuid4()en cada POST, cada intento es "una intención distinta": la UNIQUE no te salva (claves distintas) y tienes dos pagos. El bug es semántico: la clave debe venir del CLIENTE (la intención es del cliente, no del servidor). - Hoy tu API respondería 500 (IntegrityError sin capturar) o 201 dos veces según el caso: ambos mal. Correcto: 200 con la respuesta original (idempotente) — el reintento es normal, no un error.
Ejercicio 2 — La clave real
def with_idempotency(view):
def wrapped(request, *args, **kwargs):
key = request.headers.get("Idempotency-Key")
if not key:
return problem_response(status=400, title="Idempotency-Key requerida")
body_hash = hashlib.sha256(request.body).hexdigest()
with transaction.atomic():
try:
saved = IdempotencyKey.objects.get(pk=key)
if saved.request_hash != body_hash:
return problem_response(status=422, title="La clave es de otra intención")
return Response(saved.body, status=saved.status)
except IdempotencyKey.DoesNotExist:
response = view(request, *args, **kwargs) # procesa UNA vez
IdempotencyKey.objects.create(
pk=key, request_hash=body_hash,
status=response.status_code, body=response.data)
return response
return wrappedPunto fino: entre el GET-que-no-existe y el INSERT hay ventana — la PK de la clave (UNIQUE) hace que el segundo commit del mismo key falle y deba releer y devolver la guardada (patrón upsert con on_conflict). 3. Mismo POST ×2 con misma key: respuesta idéntica, 1 Payment.
Ejercicio 3 — Idempotentes por diseño
- Cancelar dos veces: la primera 204; la segunda
cancel()devuelve False → también 204 (el estado ya es el pedido). Idempotente y honesto. - PUT absoluto ×2 = mismo estado. PATCH
{"price": "+2"}×2 = +4: relativo = no idempotente. Regla: operaciones absolutas en PUT/PATCH si quieres reintentos seguros.
Ejercicio 4 — Webhook
Payment.objects.filter(gateway_event_id=payload["id"]).exists()→ respond 200 sin crear. El 4º intento igual. 2. Siempre 200 (o 2xx): si respondes 4xx/5xx a un duplicado, la pasarela reintenta de nuevo (su política) y te inunda; un duplicado procesado-con-éxito es 200 y punto. El 5xx solo para fallos reales (tu DB caída), que son los que DEBEN reintentarse.
Ejercicio 5 — Versionado
path("api/v1/", include(...))yapi/v2/con el serializer v2 (starts_atcon tz). Conviven: clientes viejos intactos.Deprecation: true+Sunset: <fecha HTTP-date>en respuestas v1. Métrica: contador por (versión, endpoint) etiquetado por client_id — cuando v1 < 1% del tráfico por 30 días y no hay clientes de pago sin migrar, se apaga.
Resumen del profesor
- La intención la nombra el cliente: Idempotency-Key + hash del cuerpo + respuesta guardada, todo en la transacción del efecto.
- Los verbos seguros (PUT/DELETE/GET) son idempotentes por diseño si no guardas secretos semánticos (+2).
- Webhooks at-least-once: duplicado = 200 y silencio; 5xx solo para fallos reales.
- Compatibilidad hacia atrás: añadir es seguro, quitar/cambiar es versión. La deprecación se anuncia y se mide, no se improvisa.
Después: Lección 15 — Validación y contratos con OpenAPI.