ELRM developer Bac à sable

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, avec details.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.