Idempotenz

Sichere Retries für Schreiboperationen über den Header Idempotency-Key (§1.3). Geeignete Routen, TTL, Speicherung und Retry-Semantik.

6 Min. Lesezeit
idempotencyidempotency-keyretries

Netzwerkausfälle passieren. Dieselbe mutierende Anfrage zweimal zu senden kann Duplikate erzeugen (zwei gestartete Runs, zwei generierte Keys, doppelt berechnete Kostensätze). Die Idempotenz-Schicht von CodeCourier löst das: Hängen Sie einen Header Idempotency-Key an, und der Server garantiert eine einzige Seiteneffekt, auch wenn der Client erneut sendet.

So funktioniert es

  1. Der Client erzeugt pro logischer Operation einen eindeutigen Key (UUIDv4 empfohlen) und sendet ihn im Header Idempotency-Key.
  2. Der Server hasht Key + Methode + Pfad + Body; bei der ersten Anfrage führt er den Handler aus und speichert die vollständige Antwort.
  3. Bei jeder Wiederholung mit demselben Key wird die gespeicherte Antwort Byte für Byte abgespielt. Kein doppelter Seiteneffekt.
  4. Trifft eine Wiederholung mit demselben Key, aber abweichendem Body ein, gibt der Server 409 idempotency_mismatch zurück.

TTL

Idempotenz-Datensätze werden 24 Stunden aufbewahrt. Danach wird der Key vergessen, und ein erneutes Abspielen führt eine frische Anfrage aus. Planen Sie Retries so, dass sie deutlich innerhalb dieses Fensters abgeschlossen sind.

Geeignete Routen

Alle POST-Mutations-Endpunkte akzeptieren den Header. Nennenswerte Beispiele:

  • POST /api/v1/runs/start
  • POST /api/v1/workflows/create
  • POST /api/v1/issues/create
  • POST /api/v1/project/api-keys/generate
  • POST /api/v1/sandboxes/create
  • POST /api/v1/cost-rates/upsert
  • POST /api/v1/recurring-tasks/create

GET, HEAD und DELETE sind per Definition idempotent; der Header wird für sie ignoriert (niemals ein Fehler).

PATCH-Endpunkte

Nur POST-Routen sind formal als Unterstützer des Headers Idempotency-Key dokumentiert. Der einzige aktuell verfügbare PATCH-Endpunkt wird vom Server als Schreiboperation behandelt und akzeptiert den Header nach bestem Bemühen:

  • PATCH /api/v1/webhooks/endpoints/{id} - unterstützt

Bei zukünftigen PATCH-Routen gehen Sie davon aus, dass Idempotenz nicht garantiert ist, sofern dies nicht ausdrücklich für die jeweilige Route dokumentiert ist. Clients, die neue PATCH-Endpunkte integrieren, sollten clientseitige Deduplizierung implementieren (z. B. einen Cache der laufenden Anfragen, geschlüsselt nach Ressourcen-ID + Body-Hash), bis erstklassige Unterstützung bestätigt ist.

Beispiel

curl

IDEM=$(uuidgen)

curl -X POST https://<your-deployment>.convex.site/api/v1/runs/start \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEM" \
  -d '{ "workflowId": "wf_abc", "input": { "topic": "hello" } }'

TypeScript

import { randomUUID } from "node:crypto";

async function startRun(workflowId: string, input: unknown) {
  const idempotencyKey = randomUUID();
  const attempt = async () =>
    fetch("https://<your-deployment>.convex.site/api/v1/runs/start", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.CC_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ workflowId, input }),
    });

  // Retry up to 3x with exponential backoff - safe because of Idempotency-Key
  for (let i = 0; i < 3; i++) {
    const res = await attempt();
    if (res.ok) return res.json();
    if (res.status < 500) throw new Error(await res.text());
    await new Promise((r) => setTimeout(r, 2 ** i * 500));
  }
  throw new Error("exhausted retries");
}

Python

import os, uuid, time, requests

def start_run(workflow_id: str, input_payload: dict):
    idem = str(uuid.uuid4())
    headers = {
        "Authorization": f"Bearer {os.environ['CC_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": idem,
    }
    body = {"workflowId": workflow_id, "input": input_payload}

    for i in range(3):
        r = requests.post(
            "https://<your-deployment>.convex.site/api/v1/runs/start",
            headers=headers, json=body, timeout=30,
        )
        if r.ok:
            return r.json()
        if r.status_code < 500:
            r.raise_for_status()
        time.sleep(2 ** i * 0.5)
    raise RuntimeError("exhausted retries")

Retry-Semantik

  • Gleicher Key + gleicher Body→ gespeicherte Antwort abspielen (200 OK, falls Original erfolgreich; ansonsten der ursprüngliche Fehler).
  • Gleicher Key + abweichender Body 409 idempotency_mismatch. Für die neue Operation einen neuen Key generieren.
  • Inflight-Duplikat(zwei Retries treffen aufeinander) → die spätere Anfrage blockiert kurz und spielt dann die Antwort der ersten ab.

Best Practices

  • Erzeugen Sie den Key vor dem ersten Versuch und verwenden Sie ihn für alle Retries derselben logischen Operation wieder.
  • Verwenden Sie UUIDv4 oder eine ULID. Leiten Sie den Key niemals aus veränderlichem Request-Zustand ab.
  • Verwenden Sie Keys nicht über logisch unterschiedliche Operationen hinweg wieder.
  • Loggen Sie den Key zusammen mit der Antwort zur Trace-Korrelation.

Verwandt