# Idempotence

https://gorally.games/developpeurs/idempotence

Une écriture peut échouer à moitié : ta connexion tombe. Ton logiciel ne sait alors pas si Rally a reçu la requête. Une `Idempotency-Key` te laisse la renvoyer sans risque de doublon.

## Sur quelles requêtes

Envoie une `Idempotency-Key` sur chaque `POST`. Elle est facultative, mais sans elle un nouvel envoi crée un doublon.

- Prends une valeur unique par requête, par exemple un UUID.
- Elle n’est jamais vide et fait 255 caractères au plus. Sinon l’API répond `INVALID_INPUT`.
- Rally la garde 24 heures.

**curl**

```bash
curl https://gorally.games/v1/leagues \
  -X POST \
  -H "Authorization: Bearer $RALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6a1f0c3e-creer-ligue-mardi" \
  -d '{"name":"Ligue du mardi","sport":"CURLING","timeZone":"America/Toronto"}'
```

**JavaScript**

```js
const response = await fetch('https://gorally.games/v1/leagues', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6a1f0c3e-creer-ligue-mardi',
  },
  body: JSON.stringify({
    "name": "Ligue du mardi",
    "sport": "CURLING",
    "timeZone": "America/Toronto"
  }),
});
const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://gorally.games/v1/leagues",
    headers={
        "Authorization": f"Bearer {os.environ['RALLY_API_KEY']}",
        "Idempotency-Key": "6a1f0c3e-creer-ligue-mardi",
    },
    json={
        "name": "Ligue du mardi",
        "sport": "CURLING",
        "timeZone": "America/Toronto",
    },
    timeout=10,
)
data = response.json()
```

## Ce que fait l’API

Rally compare la clé et le corps de la requête, octet pour octet.

- Première fois : la requête s’exécute, et sa réponse est mémorisée.
- Même clé, même corps : Rally rejoue la réponse d’origine, sans rien réécrire. Le rejeu porte l’en-tête `Idempotent-Replayed: true`.
- Même clé, autre corps : l’API refuse.

Un refus rendu pendant l’exécution est rejoué comme un succès : droits manquants, élément introuvable, conflit. Une `5xx` n’est jamais mémorisée : elle veut dire « réessaie ».

Un corps refusé à la lecture ne réserve pas la clé : JSON mal formé, champ invalide ou manquant. Corrige la requête et renvoie-la avec la même clé : elle s’exécute, sans `Idempotent-Replayed`.

Le rejeu porte le `request_id` de ta nouvelle requête, pas celui de l’originale.

## Les trois erreurs

Elles répondent toutes `409`.

| Code | Cause | Que faire |
|---|---|---|
| `IDEMPOTENCY_KEY_REUSED` | Cette clé a déjà servi avec un corps différent. | Prends une clé neuve pour cette nouvelle requête. |
| `IDEMPOTENT_REQUEST_IN_FLIGHT` | Une requête avec cette clé est encore en cours. | Réessaie dans un instant, avec la même clé. |
| `IDEMPOTENCY_OUTCOME_UNKNOWN` | Une requête avec cette clé s’est interrompue sans issue connue. | Relis l’élément pour savoir s’il existe, puis reprends avec une clé neuve. |

Pour `IDEMPOTENCY_OUTCOME_UNKNOWN`, Rally ne rejoue pas : une connexion perdue juste après l’enregistrement ressemble à une perdue juste avant. Rejouer risquerait le doublon que la clé sert à éviter.

## Pas sur les PATCH

N’envoie jamais d’`Idempotency-Key` sur un `PATCH`. L’API l’ignore.

`PATCH /matches/{matchId}` se protège autrement, par un verrou de révision. Tu envoies `expectedRevision`, la révision que tu as lue. Si quelqu’un a écrit entre-temps, l’API répond `CONFLICT`.

**curl**

```bash
curl https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c \
  -X PATCH \
  -H "Authorization: Bearer $RALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expectedRevision":3,"event":"COMPLETER","score":{"a":7,"b":5,"unitsPlayed":8}}'
```

**JavaScript**

```js
const response = await fetch('https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "expectedRevision": 3,
    "event": "COMPLETER",
    "score": {
      "a": 7,
      "b": 5,
      "unitsPlayed": 8
    }
  }),
});
const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.patch(
    "https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c",
    headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
    json={
        "expectedRevision": 3,
        "event": "COMPLETER",
        "score": {
            "a": 7,
            "b": 5,
            "unitsPlayed": 8,
        },
    },
    timeout=10,
)
data = response.json()
```

Après un `CONFLICT`, relis la rencontre et décide. Ne renvoie pas à l’aveugle : tu écraserais le travail de l’autre programme.

## Et ensuite

- Lis le guide « Pagination » pour parcourir les listes.
- Garde le guide « Erreurs » sous la main pour les autres codes.
