L'idempotence dans les APIs — Pourquoi votre POST a été déclenché deux fois et comment le corriger
Un paiement facturé deux fois, une commande dupliquée ou un utilisateur créé trois fois — ces symptômes proviennent d'un même contrat API manquant. Voici ce que signifie l'indépendance, pourquoi le POST la casse et comment les clés d'indépendance la résolvent.
Votre utilisateur a cliqué sur "Payer maintenant". Le spinner a tourné. Timeout réseau. Votre logique de réessai a été activée. Le paiement a été effectué. Puis la requête initiale a également été complétée côté serveur — et le paiement a été effectué une seconde fois. $200 retiré de compte client, et une demande de support en attente pour le matin.
Ce n'est pas un cas rare. C'est ce qui se produit quand on ne pense pas à l'idempotence avant qu'elle ne soit nécessaire. Voyons comment résoudre cela.
Ce que signifie réellement l'idempotence
Ce mot provient des mathématiques. Une opération est idempotente si elle produisant le même résultat lorsqu'elle est appliquée plusieurs fois que lorsqu'elle est appliquée une seule fois. f(f(x)) = f(x).
Dans le contexte des API : appeler la même URL avec le même objectif N fois doit laisser le système dans le même état que l'appel effectué une seule fois. La réponse peut être un résultat en cache, mais les effets secondaires — les écritures dans la base de données, les paiements, les emails — doivent se produire une seule fois uniquement.
C'est une garantie sur ce que fait votre serveur ne, pas seulement sur ce qu'il retourne.
Méthodes HTTP : qui est sûr et qui ne l'est pas
La spécification HTTP désigne certaines méthodes comme idempotentes par définition. Voici l'analyse pratique :
| Méthode | Idempotente ? | Pourquoi |
|---|---|---|
GET | ✅ Oui | Lecture uniquement. Aucun effet secondaire par définition. |
HEAD | ✅ Oui | Même que GET, sans corps retourné. |
PUT | ✅ Oui | « Mettre ce ressource dans exactement cet état ». Appeler deux fois donne le même résultat. |
DELETE | ✅ Oui | La ressource est supprimée après le premier appel. Les appels suivants trouvent rien à supprimer. (Le code de statut peut varier — 204 vs 404 — mais l'état du serveur ne change pas.) |
POST | ❌ Non | « Traiter ce payload ». Ce que cela signifie dépend du serveur. Deux appels POST créent généralement deux ressources ou déclenchent deux effets secondaires. |
PATCH | ⚠️ Dépend | Une mise à jour relative ("increment count by 1") n'est pas idempotente. Une mise à jour absolue ("set count to 5") l'est. Votre implémentation détermine laquelle. |
Le problème est que la plupart des opérations commerciales réelles — payer un paiement, créer une commande, envoyer une notification — s'adaptent naturellement à POST. Et POST ne fournit aucune garantie d'idempotence par défaut.
La séquence qui vous permet
Voici le mode classique d'échec, étape par étape :
Client Network Server
|
|--- POST /payments ------>| |
| |--- (delivered) -------->|
| | processing...
| | card charged ✓
|<-- (connection drops) ---| response queued
|
| [retry logic kicks in]
|
|--- POST /payments ------>| |
| |--- (delivered) -------->|
| | processing...
| | card charged ✓ (again)
|<------- 200 OK ----------|<------------------------|
|
[client sees: one charge. server did: two charges]
L'utilisateur n'a jamais vu la première réponse. D'un point de vue de l'utilisateur, la requête a échoué. D'un point de vue du serveur, elle a réussi deux fois. C'est un classique des systèmes distribués — l'utilisateur et le serveur ont divergé sur ce qui s'est produit.
Ce n'est pas seulement les processeurs de paiement. C'est la création de commandes, l'inscription des utilisateurs, l'envoi d'e-mails, les réservations d'inventaire — tout ce qui a des conséquences réelles si « on exécute deux fois ».
Les clés d'idempotence : le modèle Stripe
Stripe a popularisé l'approche que la plupart des API de paiement utilisent aujourd'hui. Le client génère une clé unique avant d'envoyer la requête et l'attache en tant que header. Le serveur utilise cette clé pour éviter les doublons.
Côté client :
// Generate once, before any retry loop
const idempotencyKey = crypto.randomUUID();
async function chargeWithRetry(amount, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // same key on every retry
},
body: JSON.stringify({ amount, currency: 'usd' }),
});
if (response.ok) return await response.json();
// Don't retry on client errors (4xx)
if (response.status < 500) throw new Error(`Client error: ${response.status}`);
} catch (err) {
if (attempt === retries - 1) throw err;
await sleep(Math.pow(2, attempt) * 1000); // exponential backoff
}
}
}
La clé doit être générée avant dans chaque boucle de réessai — c'est le but de tout cela. Si vous générez un nouvel UUID à chaque tentative, vous avez entièrement détruit le mécanisme.
Un UUID v4 fonctionne bien ici. Si vous devez le générer rapidement pendant les tests, IO Tools' générateur d'UUID vous permet de produire un groupe d'UUID sans avoir à importer une bibliothèque.
Côté serveur : stocker la réponse, la retourner en cas de répétition
Le rôle du serveur est conceptuellement simple : vérifier si cette clé a déjà été vue ; si oui, retourner la réponse stockée ; si non, traiter et stocker.
// Express + Redis
app.post('/api/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idem:${idempotencyKey}`);
if (cached) {
const { status, body } = JSON.parse(cached);
return res.status(status).json(body);
}
}
const result = await paymentProcessor.charge(req.body);
const responseBody = { id: result.id, status: result.status };
if (idempotencyKey) {
// 24-hour TTL — same window Stripe uses
await redis.setex(
`idem:${idempotencyKey}`,
86400,
JSON.stringify({ status: 200, body: responseBody })
);
}
res.json(responseBody);
});
Quelques détails qui comptent :
- Les échecs doivent être stockés, pas seulement les succès. Si le paiement échoue (carte refusée), stockez également cette réponse d'échec. Sinon, une tentative de réessai réessayera le paiement même s'il a déjà retourné une erreur — ce que vous essayez de éviter.
- Vérifier la cohérence du corps. Stripe retourne 422 si la même clé est réutilisée avec des paramètres différents. Cela détecte les bugs où vous réutilisez accidentellement une clé entre différentes opérations.
- Gérer les doublons en cours. Deux requêtes avec la même clé arrivant simultanément nécessitent une coordination — traiter une et retourner 409 sur l'autre, ou utiliser un verrou distribué. SETNX de Redis est le modèle courant ici.
- Définir une durée de validité raisonnable. Après l'expiration, les clés peuvent être recyclées et les nouvelles requêtes avec la même clé sont traitées comme fraîches. Ne les stockez pas indéfiniment — votre cache risque de s'agrandir.
Des bugs côté client qui lancent deux fois un POST sans réessai
Les clés d'idempotence protègent contre les réessais au niveau du réseau. Elles n'aident pas quand le client lance deux requêtes séparées et indépendantes. Des cas courants :
- Double-clique sur le bouton d'envoi. L'utilisateur clique, voit rien, clique à nouveau. Désactivez le bouton immédiatement au premier clic — pas après la réponse. La période entre le clic et la réponse est exactement celle où le deuxième clic arrive.
- Double invocation de React StrictMode. React 18 Strict Mode exécute les effets deux fois en développement pour détecter les bugs. Si vous lancez un POST dans un
useEffectsans nettoyage, vous verrez des requêtes dupliquées en développement. Ce n'est pas le cas en production, mais cela peut masquer le problème réel. - Réessai de formulaire dans le navigateur. Envoyer → naviguer → revenir → avancer. Certains navigateurs proposent « réessayer ? », d'autres le font directement. Le modèle POST-REDIRECT-GET (retour d'une redirection après un POST) élimine entièrement ce problème.
- Des conditions de concurrence dans les gestionnaires d'événements. Un clic et une touche Entrée rapide déclenchent tous deux le gestionnaire d'envoi avant que la première réponse ne soit reçue et désactivent le formulaire.
- Réessai en arrière-plan dans les applications mobiles. Les mécanismes de récupération en arrière-plan sur iOS et Android peuvent répéter une requête déjà envoyée par l'application. Générez des clés d'idempotence au moment de l'intention utilisateur, stockez-les localement si nécessaire, et supprimez-les uniquement après une confirmation de succès.
Une alternative à considérer : utiliser une PUT vers une URL avec un UUID
Si vous contrôlez les deux extrémités de l'API, il existe une option structuralement plus propre pour la création de ressources : laissez le client attribuer l'identifiant de la ressource et utilisez une PUT au lieu d'une POST.
# Instead of this (POST, not idempotent):
POST /orders
{"amount": 99.00, "items": [...]}
# Do this (PUT to client-generated UUID, idempotent by spec):
PUT /orders/7f3b9c2a-4e5d-4f8b-9a1c-2d3e4f5a6b7c
{"amount": 99.00, "items": [...]}
La méthode PUT est idempotente selon la spécification HTTP — « mettre cette ressource dans cet état » signifie que répéter la même PUT n'a aucun effet supplémentaire une fois que la ressource existe. Le serveur gère le doublon avec INSERT ... ON CONFLICT DO NOTHING ou équivalent.
Ce modèle fonctionne bien pour la création de commandes, la gestion des brouillons, et toute ressource où la cohérence de l'identifiant lors des réessais est importante. Il ne fonctionne pas lorsque le tiers (un processeur de paiement) attribue l'identifiant, ou lorsque l'effet secondaire doit se produire côté serveur avant que vous ne sachiez l'identifiant.
Ce que ressemble réellement l'implémentation de Stripe
La documentation sur les clés d'idempotence de Stripe vaut la peine d'être lue en détail. Quelques spécificités importantes dans la pratique : Format de la clé :
- Toute chaîne de caractères de jusqu'à 255 caractères, scannée dans votre compte Stripe. Deux comptes différents utilisant la même chaîne ne s'interfèrent pas. Fenêtre de 24 heures :
- Les clés expireront après 24 heures. Un réessai après expiration est traité comme une requête fraîche — utile à connaître si vous construisez des flux longs. Mauvaise cohérence du corps = 422 :
- Même clé, paramètres différents → Stripe retourne 422 Unprocessable Entity. C'est le comportement correct ; il détecte le bug où vous réutilisez accidentellement une clé. Déduplication en parallèle :
- Si deux requêtes avec la même clé arrivent simultanément, Stripe traite une et retourne 409 Conflict sur l'autre. Réessayer le 409 après un court délai. Stockage des échecs :
- Si le paiement échoue (carte refusée), Stripe stocke cet échec. Réessayer avec la même clé retourne la même refus. Vous avez besoin d'une nouvelle clé pour essayer une autre carte — ce qui est correct, car l'essai précédent était une opération complète et intentionnelle. Vérifier l'idempotence sans un ensemble complet de tests d'intégration
La méthode la plus rapide pour vérifier le comportement consiste à envoyer deux fois votre endpoint avec la même clé et à vérifier que la deuxième requête retourne la réponse en cache sans déclencher à nouveau l'effet secondaire.
IO Tools' outil de construction de commandes cURL facilite la construction de la requête avec des en-têtes personnalisés — incluant — sans avoir à mémoriser la syntaxe des flags cURL. Idempotency-Key Scénarios à couvrir :
Même clé + même corps, dans la fenêtre de validité → réponse en cache retournée, effet secondaire déclenché une fois
- Même clé + paramètres différents → 422 (ou votre statut de conflit choisi)
- Même clé après expiration de la fenêtre de validité → traitée comme une requête fraîche
- Aucune clé fournie → traitée normalement (déterminez à l'avance si les clés sont obligatoires ou optionnelles)
- Deux requêtes simultanées avec la même clé → une est traitée, l'autre reçoit 409
- POST n'est pas idempotent, et cette période entre « requête envoyée » et « réponse reçue » est là où les effets secondaires dupliqués existent. La solution n'est pas complexe : générer un UUID avant chaque boucle de réessai, l'envoyer en tant qu'en-tête, et stocker la réponse côté serveur, clé par en-tête. Les parties difficiles sont les détails — le stockage des échecs, la gestion des doublons simultanés, le choix de la durée de validité — mais le modèle de base est simple assez pour être ajouté à tout endpoint qui importe.
AES-256-GCM
Ajouter le support des clés d'idempotence avant le premier trafic en production. Retoucher cela après un incident de paiement double est un moment significativement pire pour apprendre cette leçon.
L'idempotence dans les API — Pourquoi votre POST a été exécuté deux fois et comment le corriger 2
Installez nos extensions
Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide
恵 Le Tableau de Bord Est Arrivé !
Tableau de Bord est une façon amusante de suivre vos jeux, toutes les données sont stockées dans votre navigateur. D'autres fonctionnalités arrivent bientôt !
Outils essentiels
Tout voir Nouveautés
Tout voirMise à jour: Notre dernier outil a été ajouté le 17 juin 2026
