L'importance de l'idempotence dans vos API
Une requête idempotente garantit que son exécution multiple produit le même effet que son exécution unique. Pourquoi c'est fondamental, et comment l'implémenter en Go.
Vous avez un formulaire de paiement. L’utilisateur clique sur “Payer”, la requête part, le réseau lag, l’utilisateur ne voit rien se passer, il reclique. Votre API crée deux commandes. Deux prélèvements. Le client râle, le support vous appelle, et vous passez votre après-midi à annuler un doublon en base.
Ce problème a un nom : l’absence d’idempotence. Une requête idempotente, c’est une requête qui produit le même résultat qu’on l’exécute une fois ou dix fois. Pas de doublon, pas d’effet de bord.
Ça arrive plus souvent qu’on croit
Un timeout réseau et le client HTTP retente automatiquement. Un utilisateur impatient qui double-clique. Un job asynchrone qui redémarre et rejoue ses messages. Un webhook qui arrive deux fois parce que le premier n’a pas été acquitté assez vite.
Si votre API n’est pas idempotente, chacun de ces cas crée des doublons. Et les doublons, c’est le genre de bug silencieux qu’on découvre trois mois plus tard en regardant les chiffres de CA — “sympa le fake CA”.
Un exemple tout bête
Prenons un système de commande basique (on est d’accord, c’est juste pour illustrer) :
package main
import (
"encoding/json"
"net/http"
"sync"
)
type Order struct {
ID int `json:"id"`
Item string `json:"item"`
}
var (
orders = []Order{}
nextID = 1
mutex = &sync.Mutex{}
)
func createOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
var order Order
if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
mutex.Lock()
order.ID = nextID
nextID++
orders = append(orders, order)
mutex.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}
Chaque appel crée une nouvelle commande avec un nouvel ID. Si le client retente la requête — timeout, double-clic, peu importe — vous avez un doublon en base. Et personne ne s’en rend compte avant que la compta ne pose des questions.
Rendons cette route idempotente
L’approche la plus simple consiste à utiliser un identifiant unique pour chaque requête fourni par le client (ici requestId) :
type Order struct {
ID int `json:"id"`
Item string `json:"item"`
RequestID string `json:"request_id"`
}
var (
orders = map[string]Order{}
nextID = 1
mutex = &sync.Mutex{}
)
func createOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
var order Order
if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
mutex.Lock()
defer mutex.Unlock()
// Si on a déjà traité cette requête, on renvoie la réponse initiale
if existingOrder, found := orders[order.RequestID]; found {
json.NewEncoder(w).Encode(existingOrder)
return
}
order.ID = nextID
nextID++
orders[order.RequestID] = order
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}
Si un client soumet plusieurs fois une requête avec le même requestId, l’API retournera simplement la réponse initiale au lieu de créer un nouvel enregistrement.
D’autres approches
Le requestId côté client, c’est la méthode la plus propre. Mais ce n’est pas toujours possible — vous ne maîtrisez pas forcément le client, ou l’API est publique.
Hash du body — Vous calculez un SHA-256 du corps de la requête et vous l’utilisez comme clé de déduplication. Simple, mais attention aux requêtes légitimement identiques (deux commandes du même produit par deux utilisateurs différents).
Contrainte d’unicité en BDD — Parfois le plus pragmatique. Un UNIQUE sur le bon champ et PostgreSQL fait le travail pour vous. Pas besoin de code applicatif.
Utiliser PUT au lieu de POST — PUT est idempotent par définition dans la spec HTTP. Si votre cas d’usage le permet, c’est la solution la plus élégante. Le client fournit l’identifiant, le serveur crée ou met à jour — pas de doublon possible.
Dans la vraie vie, j’utilise souvent une combinaison : un requestId en header pour les clients qu’on maîtrise, et une contrainte d’unicité en BDD comme filet de sécurité. Ceinture et bretelles.
Besoin d'aide sur ce sujet ?
Réserver un créneau