Ir al contenido principal

Reintenta una petición sin doble efecto

La idempotencia permite reintentar una petición POST sin temer un doble efecto. Un cliente que pierde la respuesta por timeout puede reintentar con la misma clave de idempotencia, y la API devuelve la respuesta almacenada en lugar de ejecutar de nuevo el handler. CaptainDNS sigue la convención de Stripe, con una ventana de validez de 24 horas.

Por qué usar la idempotencia

Las redes no son fiables. Un cliente puede:

  • Enviar una petición que tiene éxito en el servidor pero cuya respuesta se pierde en camino.
  • Caer en medio de un reintento automático.
  • Ver un timeout en el balanceador cuando la API ya procesó la petición.

Sin idempotencia, debes elegir entre:

  • No reintentar, a riesgo de perder operaciones legítimas.
  • Reintentar a ciegas, a riesgo de ejecutar varias veces la misma operación.

La idempotencia ofrece una tercera vía: reintentar con seguridad.

Cómo usarla

Añade una cabecera Idempotency-Key a tu petición POST:

POST /public/v1/deliverability/score HTTP/1.1
Host: api.captaindns.com
Authorization: Bearer cdns_live_a3f2XK7mN9QrVtZ4yP1sH6eL8cF2dB5aR3gW7kJxM
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{"email":"contact@captaindns.com"}

La clave debe ser una cadena opaca de entre 1 y 255 caracteres ASCII imprimibles sin espacios. Los UUIDv4 o ULID siguen siendo recomendados por buenas prácticas (entropía suficiente, colisiones estadísticamente imposibles), pero cualquier identificador único del lado cliente sirve.

Qué pasa en el servidor

En la primera petición:

  1. CaptainDNS calcula el SHA-256 del body de la petición.
  2. Tras el éxito, guarda la asociación entre la clave de idempotencia y el hash del body.
  3. Captura el status y el body de la respuesta antes de devolverla al cliente.

En la segunda petición con la misma clave:

  1. El middleware encuentra el registro existente.
  2. Si el hash del nuevo body coincide: replay. La respuesta almacenada se devuelve tal cual, con X-Idempotent-Replay: true.
  3. Si el hash difiere: conflicto. La API devuelve 409 IDEMPOTENCY_CONFLICT con un mensaje explícito.

Un replay no dispara el handler subyacente y no escribe una segunda línea de uso: la respuesta almacenada vuelve tal cual. Ahora bien, atraviesa todas las etapas que lo preceden. El control de créditos y el token bucket por clave se aplican antes de que la idempotencia intervenga: un replay consume, por tanto, un token de rate limit y un crédito, igual que una llamada corriente. La idempotencia te protege del doble procesamiento, no de la doble facturación. Dimensiona tus reintentos con eso en mente.

Ventana de validez

Los registros viven 24 horas. Tras ese plazo, un job de purga los elimina y una clave reutilizada vuelve a ejecutar el handler con normalidad.

La ventana es un compromiso: demasiado corta, y los reintentos diferidos pierden su interés; demasiado larga, y acumulas un volumen creciente de respuestas antiguas. Veinticuatro horas cubren la mayoría de los patrones de reintento: el inmediato tras un error de red, el de los minutos siguientes a la reanudación de un job, y el programado de madrugada tras un fallo detectado en monitorización.

Métodos cubiertos

La idempotencia solo aplica a métodos que mutan o son costosos. En la práctica, todos los endpoints públicos de CaptainDNS son POST, así que todos son elegibles. Las peticiones GET no pasan por el mecanismo: ya son idempotentes por naturaleza en HTTP.

La cabecera Idempotency-Key es opcional. Si no la envías, la petición se procesa con normalidad, sin garantía de replay.

Conflictos

Un 409 IDEMPOTENCY_CONFLICT se dispara cuando:

  • Reutilizas una clave de idempotencia para una petición cuyo body difiere, aunque sea en un carácter.
  • Eso ocurre fácilmente si tu cliente regenera la petición entre dos intentos: timestamps, UUID embebidos, orden de las claves JSON.

Para evitarlo:

  • Calcula la clave antes de serializar el body, no después.
  • No mezcles llamadas distintas bajo la misma clave.
  • Si tu framework reordena las claves JSON, fija el orden a mano.

Si el conflicto es legítimo, es decir si de verdad quieres ejecutar otra operación, genera una clave nueva.

Ejemplos de uso

Reintento tras timeout de red

idempotencyKey = uuid()
for attempt in 1..3:
    try:
        response = post(url, body, headers={"Idempotency-Key": idempotencyKey})
        return response
    except TimeoutError:
        sleep(2 * attempt)
raise

Cada reintento usa la misma clave. Si una de las llamadas ya tuvo éxito en el servidor, las siguientes devuelven la respuesta almacenada.

Deduplicación en cola de jobs

job = fetch_next_job()
idempotencyKey = hash(job.id)
response = post(url, job.payload, headers={"Idempotency-Key": idempotencyKey})
mark_job_done(job.id, response)

Batch con dedup en el servidor

for domain in domains:
    idempotencyKey = "batch-" + run_id + "-" + domain
    post(url, {"domain": domain}, headers={"Idempotency-Key": idempotencyKey})

Limitaciones

  • 24 h de almacenamiento: más allá, el replay ya no es posible.
  • Scope por clave API: dos claves distintas con el mismo valor no colisionan.
  • Sin reanudación entre servidores: si usas varios backends distintos, comparten la misma API pero deben coordinar entre ellos las claves de idempotencia.
  • Identidad estricta del body: un espacio extra rompe el hash.

Próximos pasos

Sigue con los códigos de error para entender cómo reaccionar a 409 IDEMPOTENCY_CONFLICT, o consulta la referencia OpenAPI.