Idempotencia
El problema
Sección titulada «El problema»Usted envía un POST para crear una venta. La conexión se cae antes de que la respuesta vuelva. Y ahora: ¿la venta se creó o no?
Sin ayuda, las dos salidas son malas. Si repite, puede crear dos ventas. Si no repite, puede no haber creado ninguna.
Cómo resolverlo
Sección titulada «Cómo resolverlo»Envíe un encabezado Idempotency-Key con un valor suyo, único para esa operación:
curl -X POST "https://api.fatureihoje.com/public/v1/clients" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6b7f0e8a-4c21-4d9e-9f3a-71c5d8b2e044" \ -d '{ "name": "Ana Souza" }'El encabezado es el mismo en toda ruta de creación.
La API guarda la respuesta junto con esa clave. Si la misma clave llega de nuevo con la misma solicitud, devuelve la respuesta guardada, con el mismo estado y el mismo cuerpo, sin ejecutar nada otra vez. La respuesta repetida viene con el encabezado Idempotent-Replayed: true, y así usted sabe que el efecto ocurrió la primera vez.
Entonces la regla de reintento queda simple: repita la misma solicitud, con la misma clave, cuantas veces necesite.
Cuándo usarla
Sección titulada «Cuándo usarla»- Siempre que un
POSTcree algo, y sobre todo cuando repite la llamada después de un error de red, un tiempo agotado, un 500 o un 503. - En una cola y en un trabajo que corre de nuevo después de fallar. Genere la clave junto con la tarea y guárdela con ella, no en el momento de la llamada: generar una clave nueva en cada intento anula el mecanismo.
El valor puede ser cualquier texto. Un UUID versión 4 por operación es la opción más simple, y es lo que generan los ejemplos de abajo.
La huella de la solicitud
Sección titulada «La huella de la solicitud»La clave sola no basta. La API también guarda una huella de la solicitud, hecha de cuatro cosas:
- el método;
- la ruta, en minúsculas y sin barra final;
- la consulta, la parte después del
?; - el cuerpo, comparado por contenido y no por texto, así que el orden de los campos del JSON no importa.
Si la clave llega de nuevo con la misma huella, es repetición: vuelve la respuesta guardada. Si la huella es distinta, no es repetición, es un error, y la respuesta es 422 idempotency_key_reused.
Los dos errores
Sección titulada «Los dos errores»| Código | Estado | Cuándo | Qué hacer |
|---|---|---|---|
idempotency_key_reused | 422 | Misma clave, solicitud distinta | Genere una clave nueva para esta operación |
idempotency_in_progress | 409 | La primera solicitud con esa clave todavía está en curso | Espere unos segundos y repita la misma solicitud, con la misma clave |
En el 409, no genere una clave nueva. La primera llamada todavía puede terminar bien, y una clave nueva ejecutaría la operación una segunda vez, que es exactamente lo que este mecanismo existe para evitar.
Si el procesamiento falla, la clave se libera al instante, y el siguiente intento con ella corre de verdad. La reserva también tiene plazo: si el proceso muere a mitad de camino, vence en pocos minutos y la clave vuelve a aceptar intentos, en vez de quedar trabada por un día.
Los límites
Sección titulada «Los límites»| Límite | Valor |
|---|---|
| Cuánto tiempo queda guardada la respuesta | 24 horas |
| Tamaño máximo del registro guardado | 256 KB |
| Tamaño máximo del cuerpo de la solicitud | 1 MB |
Las 24 horas cuentan desde la primera llamada. Pasado ese plazo, la misma clave con la misma solicitud ejecuta de nuevo, porque ya no hay nada guardado para devolver.
El tope de 256 KB es de la respuesta guardada, no de la solicitud. Una respuesta mayor que eso no queda guardada entera: la API recuerda que la operación ya corrió, y la repetición recibe 409 en vez de repetir la respuesta. El efecto ocurrió una sola vez, que es lo que importa, pero usted necesita leer el recurso para saber cómo quedó.
La respuesta guardada vale para el acceso del momento en que se generó. Si la clave, o el miembro que representa, perdió un permiso desde entonces, la repetición no devuelve ese cuerpo: sin el permiso de la ruta la respuesta es 403, y con la ruta todavía permitida pero el acceso cambiado (por ejemplo, la clave perdió finance:read y la venta guardada traía los movimientos) la respuesta es 409 conflict. En los dos casos no se ejecuta nada de nuevo; lea el recurso para saber cómo quedó.
El tope de 1 MB es del cuerpo de la solicitud y vale para toda llamada, con o sin Idempotency-Key. Por encima de él la respuesta es 413 request_too_large.
El alcance es la clave de API
Sección titulada «El alcance es la clave de API»Dos claves de API distintas tienen espacios de idempotencia separados. El mismo valor de Idempotency-Key en dos claves son dos operaciones, no una repetición, y ninguna ve la respuesta guardada de la otra.
Eso es bueno por un lado: dos sistemas de la misma empresa pueden generar el valor a su manera sin acordar nada.
Lo mismo vale para revocar una clave y crear otra: es una clave nueva, es un espacio nuevo.
Generar la clave
Sección titulada «Generar la clave»KEY=$(uuidgen | tr 'A-Z' 'a-z')echo "$KEY"uuidgen viene en macOS y en la mayoría de las distribuciones de Linux. Donde no exista, sirve cat /proc/sys/kernel/random/uuid.
import { randomUUID } from 'node:crypto';
const key = randomUUID();console.log(key);Ejecute con node clave.mjs. Guarde el valor junto con la tarea que va a hacer la llamada, no genere uno nuevo en cada intento.
<?php
function idempotencyKey(): string{ $bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));}
echo idempotencyKey(), PHP_EOL;Ejecute con php clave.php. Solo biblioteca estándar; la extensión uuid no es necesaria.
import uuid
key = str(uuid.uuid4())print(key)Ejecute con python3 clave.py. Solo biblioteca estándar.
Próximo paso
Sección titulada «Próximo paso»- Errores: el catálogo completo, con los dos códigos de idempotencia.
- Autenticación: cómo funciona la rotación de clave, que es la trampa de arriba.