Idempotencia de Peticiones
En entornos de integración distribuidos y redes móviles o inestables, es habitual que una petición HTTP sufra una pérdida de conexión, un corte de red o un timeout antes de que el cliente reciba la confirmación del servidor. Si el cliente reintenta la operación a ciegas, corre el riesgo de duplicar operaciones críticas, como crear dos veces un cliente, generar albaranes duplicados o aplicar dos veces un movimiento de stock.
Para solventar este problema de forma transparente y segura, la API de BaseGes ERP implementa un mecanismo global de idempotencia compatible con el estándar Idempotency-Key.
¿Cómo funciona?
El mecanismo se activa incluyendo la cabecera HTTP Idempotency-Key en cualquier petición mutadora (POST, PUT o PATCH).
POST /api/salesdocuments HTTP/1.1
Host: api.integral-erp.com
X-Company-Id: 1
X-Api-Key: tu-api-key
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Content-Type: application/json
{
"documentType": "BillableDeliveryNote",
"series": "26",
"customerCode": "CLI0001",
"lines": [...]
}
Se recomienda generar un identificador único universal (UUID v4) por cada operación lógica iniciada por el usuario o sistema emisor. La longitud máxima admitida para la clave es de 128 caracteres.
¿Qué ocurre si no se envía la cabecera Idempotency-Key? (Claves Sintéticas y Advertencia)
Para garantizar la compatibilidad con clientes existentes y evitar rupturas de servicio (Zero Breaking Changes), la cabecera Idempotency-Key no es estrictamente obligatoria.
Si un cliente omite la cabecera en una petición POST, PUT o PATCH:
- La petición NO se rechaza: El servidor procesa la operación con total normalidad.
- Generación automática de Clave Sintética: El servidor genera de forma determinista una clave sintética a partir del identificador de empresa, la ruta del endpoint y el hash SHA-256 del contenido (
syn_{hash}). - Cabecera de Advertencia (Warning): El servidor inyecta en la respuesta la cabecera HTTP estándar:
Warning: 299 - "Idempotency-Key header is recommended"
- Retorno de la clave asignada: La respuesta incluye la cabecera
Idempotency-Keycon el valor sintético generado (syn_...) para trazabilidad.
Aunque el mecanismo de claves sintéticas proporciona una red de seguridad básica, se recomienda encarecidamente que cada cliente genere y envíe de forma explícita su propio Idempotency-Key (UUID v4). Esto garantiza un control determinista total de los reintentos ante caídas de red o timeouts antes de recibir la respuesta.
Periodo de Vigencia y Retención
- Duración de la clave: 48 horas desde su creación original.
- Almacenamiento persistente: Las claves, su huella digital criptográfica (SHA-256) y la respuesta completa generada se guardan en la base de datos compartida del ERP (
dbo.idempotency_keysenSharedDb). - Purga automática: Un servicio en segundo plano (
IdempotencyCleanupBackgroundService) ejecuta un barrido periódico cada 1 hora para eliminar automáticamente las claves con más de 48 horas de antigüedad. - Bloqueo de concurrencia (Lock Timeout): Durante el procesamiento inicial de la petición, se adquiere un bloqueo temporal de hasta 2 minutos para evitar que dos llamadas concurrentes con la misma clave ejecuten la lógica de negocio en paralelo.
Comportamiento al Reutilizar una Clave
El comportamiento de la API depende del estado y contenido de las peticiones subsiguientes:
| Escenario | Clave | Payload (Cuerpo) | Código HTTP | Cabeceras de Respuesta | Comportamiento |
|---|---|---|---|---|---|
| Primera ejecución | Nueva | Cualquier cuerpo válido | 200 / 201 (el propio de la operación) | Normales | La petición se ejecuta de forma habitual. La respuesta y el hash SHA-256 se persisten en base de datos. |
| Reintento legítimo (Replay) | Existente | Idéntico al original | El mismo código original (200, 201, 400, etc.) | X-Cache-Lookup: HITIdempotency-Replayed: true | No se reejecuta la lógica de negocio. Se devuelve exactamente el cuerpo y código HTTP guardados en la primera llamada. |
| Reutilización indebida (Mismatch) | Existente | Diferente al original | 422 Unprocessable Entity | Normales de error | Se detecta que el hash SHA-256 del cuerpo no coincide con el registrado originalmente para esa clave. La petición se rechaza de inmediato. |
| Petición concurrente en curso | Existente | Cualquiera | 409 Conflict | Normales de error | Otra petición con la misma clave aún se está ejecutando dentro de su ventana de bloqueo de 2 minutos. |
Ejemplos de Respuestas
1. Respuesta de Reintento Exitoso (Replay Cache)
Cuando se reenvía una petición con la misma clave y el mismo cuerpo, el servidor responde con la respuesta original almacenada, garantizando que el documento no se duplica:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
X-Cache-Lookup: HIT
Idempotency-Replayed: true
{
"series": "26",
"number": "000042",
"totalAmount": 150.00
}
2. Error por Modificación de Cuerpo (422 Unprocessable Entity)
Si un integrador reutiliza accidentalmente una misma clave para enviar datos distintos:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": "Unprocessable Entity",
"message": "La clave de idempotencia ya ha sido utilizada previamente con un cuerpo de petición diferente.",
"idempotency_key": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}
3. Error por Ejecución en Concurrencia (409 Conflict)
Si dos hilos o procesos envían simultáneamente la misma clave mientras el servidor aún está procesando la primera:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "Conflict",
"message": "Una petición idéntica se está procesando actualmente. Reinténtelo en unos instantes.",
"idempotency_key": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}
Ejemplo de Integración en Código
using System.Net.Http.Json;
var client = new HttpClient { BaseAddress = new Uri("https://api.integral-erp.com") };
client.DefaultRequestHeaders.Add("X-Company-Id", "1");
client.DefaultRequestHeaders.Add("X-Api-Key", "vGZ9pL2kR7mN4QxW8jB5hY9sA1dQ6zP0");
// 1. Generar una clave única por cada transacción de negocio
var idempotencyKey = Guid.NewGuid().ToString();
var payload = new
{
documentType = "BillableDeliveryNote",
series = "26",
customerCode = "CLI0001",
lines = new[] { new { code = "ART01", quantity = 5, price = 25.0 } }
};
var request = new HttpRequestMessage(HttpMethod.Post, "/api/salesdocuments")
{
Content = JsonContent.Create(payload)
};
request.Headers.Add("Idempotency-Key", idempotencyKey);
// 2. Ejecutar la petición con reintentos seguros
var response = await client.SendAsync(request);
if (response.Headers.Contains("Idempotency-Replayed"))
{
Console.WriteLine("Aviso: Esta respuesta proviene de la caché de idempotencia.");
}
En llamadas a endpoints de solo lectura (GET, HEAD), no es necesario enviar la cabecera Idempotency-Key, ya que por definición del protocolo HTTP son operaciones idempotentes por naturaleza.