L'importance de l'idempotence dans vos api

L'idempotence est un concept fondamental dans la conception des API, en particulier lorsqu'elles exposent des endpoints permettant des opérations d'écriture (POST, PUT, DELETE).
Une requête idempotente garantit que son exécution multiple produit le même effet que son exécution unique.
Cela évite les incohérences, améliore la résilience des systèmes et facilite la gestion des erreurs.
Pourquoi l'idempotence est-elle importante ?
Les API peuvent être appelées plusieurs fois involontairement pour diverses raisons :
- Un client réseau peut retransmettre une requête en cas de timeout.
- Une interruption de connexion peut pousser l'utilisateur à réessayer l'opération.
- Un traitement asynchrone peut engendrer des doublons accidentels.
Sans idempotence, ces répétitions peuvent provoquer des effets indésirables comme des transactions en double, des facturations erronées ou la création d'objets redondants.
Petits exemples
Imaginons un bête système de commande (on est d'accord c'est juste pour l'exemple hein)
package main
import (
"encoding/json"
"fmt"
"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)
}
func main() {
http.HandleFunc("/orders", createOrder)
http.ListenAndServe(":8080", nil)
}
Dans ce cas, si un client soumet plusieurs fois la même requête (par exemple, en raison d'un timeout), une nouvelle commande sera créée à chaque fois (sympa le fake CA) avec un ID différent, ce qui entraîne des doublons indésirables.
Améliorons ça pour rendre cette route idempotente.
L'approche la plus simple consiste à utiliser un identifiant unique pour chaque requête fourni par le client (ici requestId
).
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
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()
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)
}
func main() {
http.HandleFunc("/orders", createOrder)
http.ListenAndServe(":8080", nil)
}
Ici, 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 stratégies permettent d'assurer l'idempotence
Si vous n'avez pas la possibilité d'ajouter un identifiant unique à la requête, il existe d'autres stratégies que vous pouvez mettre en place:
- Utilisation d'un hash du corps de la requête : Calculer une empreinte unique du contenu de la requête (par exemple, avec SHA-256) et l'utiliser comme clé d'identification.
- Stockage des opérations précédentes : Maintenir un journal des requêtes traitées, permettant d'ignorer les doublons.
- Gestion via la base de données : Imposer des contraintes d'unicité au niveau de la base de données, comme une clé unique sur un champ spécifique.
- Utilisation des méthodes HTTP appropriées : Favoriser
PUT
(qui est idempotent par nature) au lieu dePOST
lorsque possible.
En conclusion
L'idempotence est un principe essentiel pour la conception des API robustes.
En adoptant des stratégies comme l'utilisation d'identifiants uniques pour les requêtes, vous améliorez la fiabilité et l'intégrité des systèmes tout en réduisant les erreurs et les incohérences. Appliquer ces bonnes pratiques permet d'éviter bien des maux dans les architectures modernes.