Aller au contenu

Idempotency

L’idempotency garantit qu’une même requête de mutation, rejouée, n’a pas d’effet supplémentaire. C’est la base d’un client réseau résilient.

Tout endpoint POST qui crée ou déclenche une action accepte un header Idempotency-Key. Concrètement :

  • POST /v1/scraper/jobs
  • POST /v1/scraper/webhooks
  • POST /v1/extractor/extractions
  • POST /v1/extractor/extractor-configs
  • POST /v1/extractor/extraction-webhooks

Pour les endpoints GET/PATCH/DELETE, la sémantique HTTP suffit (les mutations sont déjà idempotentes au sens REST).

POST /v1/scraper/jobs HTTP/1.1
Idempotency-Key: 8f3a2c1e-9b6d-4f7a-a2c5-1e9d4b8a7c3f

Recommandation : UUID v4 (36 caractères). Toute string ≤ 255 caractères est acceptée, mais doit être unique côté votre application.

CasComportement
Clé jamais vueRequête traitée normalement, réponse enregistrée 24 h.
Clé déjà vue, même bodyRéponse originale retournée (status + body).
Clé déjà vue, body différent409 idempotency_mismatch.
Clé vue > 24 hLibérée, traitée comme une nouvelle requête.
async function createJob(url, attempts = 3) {
const idempotencyKey = crypto.randomUUID();
let lastErr;
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch("/v1/scraper/jobs", {
method: "POST",
headers: {
"x-api-key": process.env.EXTENTAPI_KEY,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ url }),
});
if (res.ok) return res.json();
if (res.status >= 500) throw new Error(`retryable ${res.status}`);
// 4xx -> non retryable
throw new Error(await res.text());
} catch (err) {
lastErr = err;
await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
}
}
throw lastErr;
}

Le même idempotencyKey est conservé entre les retries — c’est ce qui garantit que même si un timeout réseau cache une réponse 2xx, le serveur saura reconnaître le replay.

Ne jamais régénérer la clé entre deux retries. Ce serait sémantiquement équivalent à demander un job différent à chaque tentative.