Idempotence et concurrence#
Deux en-têtes qui évitent les doublons après un timeout et les écrasements entre votre système et le CRM.
Idempotency-Key : rejouer sans dupliquer#
Un POST qui expire côté client a peut-être abouti côté serveur. Sans protection, le réessai crée un second dossier, une seconde analyse. Pour l'éviter, envoyez sur tout POST ou PUT :
Idempotency-Key: 6c2a9f6e-2f33-4a7d-9b1a-0d3e7c1f5a42
- Une chaîne unique par intention (un UUID v4 par exemple), 120 caractères maximum.
- Même clé + même corps → la réponse d'origine est renvoyée telle quelle (statut inclus) avec l'en-tête
Idempotent-Replayed: true. L'opération n'est pas ré-exécutée. - Même clé + corps différent →
409 idempotency_key_reuse. C'est une erreur d'intégration (la clé a été réutilisée pour autre chose), pas un rejeu. - Les clés sont conservées 24 heures, et sont propres à votre clé d'API.
- L'en-tête est facultatif, mais nous le recommandons vivement sur toute création.
import uuid, httpx
def create_dossier(api: httpx.Client, body: dict) -> dict:
key = str(uuid.uuid4()) # à conserver si vous devez réessayer
for attempt in range(3):
try:
r = api.post(f"/dossiers", json=body,
headers={"Idempotency-Key": key})
r.raise_for_status()
return r.json()
except (httpx.TimeoutException, httpx.NetworkError):
continue # même clé → pas de doublon
raise RuntimeError("dossier non créé")
La comparaison des corps ignore l'ordre des champs JSON.
ETag et If-Match : ne pas écraser une modification#
Les ressources modifiables (la liste des apprenants d'un dossier) renvoient un en-tête ETag à la lecture :
ETag: W/"3f9a1c2b7d4e0a1b2c3d4e5f6a7b"
Toute mise à jour exige If-Match avec cette valeur :
PUT /dossiers/{dossier_id}/inscriptions/apprenants
If-Match: W/"3f9a1c2b7d4e0a1b2c3d4e5f6a7b"
- Si la ressource a changé entre votre lecture et votre écriture (une retouche faite dans le CRM par l'organisme, par exemple), la réponse est
412 precondition_failed, avecdetails.current_etag. Relisez, fusionnez, réessayez. - Sans
If-Match:428 precondition_required. If-Match: *est accepté et désactive le contrôle, à réserver aux scripts ponctuels.
Chaque réponse d'écriture renvoie le nouvel ETag : enchaînez les modifications sans relire.
state = api.get(f"/dossiers/{did}/inscriptions")
etag = state.headers["ETag"]
r = api.put(f"/dossiers/{did}/inscriptions/apprenants",
json={"apprenants": [...]}, headers={"If-Match": etag})
if r.status_code == 412:
# quelqu'un a modifié la liste : relire et recommencer
...
etag = r.headers["ETag"]
Les deux ensemble#
Sur un PUT avec If-Match, l'Idempotency-Key est utile aussi : un rejeu après timeout renvoie la réponse d'origine sans re-vérifier l'If-Match, ce qui est exactement ce que vous voulez, puisque c'est la même intention.