Keine Werbung mögen? Gehen Werbefrei Heute

Idempotenz in APIs – Warum Ihr POST-Befehl zweimal ausgeführt wurde und wie man es behebt

Aktualisiert am

Eine Zahlung, die zweimal berechnet wird, eine doppelte Bestellung oder ein Benutzer, der dreimal erstellt wurde – all diese sind Symptome eines fehlenden API-Vertrags. Hier erklärt, was Idempotenz bedeutet, warum POST es brechen und wie Idempotenzschlüssel es beheben.

Idempotenz in APIs — Warum Ihr POST zweimal ausgeführt wurde und wie Sie es beheben 1
ANZEIGE Entfernen?

Der Benutzer klickte auf „Zahlen jetzt“. Die Drehung begann. Netzwerkzeitüberschreitung. Ihr Wiederholungslogik wurde aktiviert. Die Gebühr wurde durchgeführt. Dann wurde auch die ursprüngliche Anfrage von der Serverseite abgeschlossen — und die Gebühr wurde erneut durchgeführt. $200 wurden aus dem Kundenkonto abgebucht, und ein Support-Ticket wartet auf Sie morgens.

Das ist keine seltene Randfall. Es passiert, wenn man vor dem Bedarf keine Idempotenz berücksichtigt. Lassen wir das beheben.

Was Idempotenz tatsächlich bedeutet

Das Wort stammt aus der Mathematik. Eine Operation ist idempotent, wenn sie mehrmals angewendet wird, das gleiche Ergebnis wie bei einer einmaligen Anwendung liefert. f(f(x)) = f(x).

Im API-Bereich: Wenn dieselbe Endpunkt mit demselben Zweck N-mal aufgerufen wird, sollte das System in demselben Zustand bleiben wie bei einer einzigen Anfrage. Die Antwort kann ein abgespeichertes Ergebnis sein, aber die Nebeneffekte — die Datenbankschreibvorgänge, die Gebühren, die E-Mails — sollten nur einmal auftreten.

Es ist eine Garantie darüber, was Ihr Server beschränkt den Cookie auf nur, nicht nur was es zurückgibt.

HTTP-Methoden: Wer ist sicher und wer nicht

Die HTTP-Spezifikation bezeichnet bestimmte Methoden als idempotent im Sinne der Definition. Hier ist die praktische Zusammenfassung:

VerfahrenIdempotent?Warum
GET✅ JaLesevorgang. Keine Nebeneffekte im Sinne der Definition.
HEAD✅ JaGleich wie GET, kein Körper zurückgegeben.
PUT✅ Ja„Setze diese Ressource genau in diesen Zustand.“ Wenn es zweimal aufgerufen wird, ergibt sich das gleiche Ergebnis.
DELETE✅ JaDie Ressource ist nach dem ersten Aufruf verschwunden. Späterer Aufrufe finden nichts mehr zu löschen. (Der Statuscode kann sich unterscheiden — 204 vs. 404 — aber der Serverzustand ändert sich nicht.)
POST❌ Nein„Verarbeite dieses Payload.“ Was das genau bedeutet, hängt vom Server ab. Zwei POST-Aufrufe erzeugen normalerweise zwei Ressourcen oder lösen zwei Nebeneffekte aus.
PATCH⚠️ AbhängigEin relativer Update ("increment count by 1") ist nicht idempotent. Ein absoluter Update ("set count to 5") ist. Ihre Implementierung bestimmt, welcher verwendet wird.

Das Problem ist, dass die meisten realen Geschäftsoperationen — eine Zahlung belasten, eine Bestellung erstellen, eine Benachrichtigung senden — sich natürlich auf POST beziehen. Und POST bietet Ihnen aus der Box keine Idempotenzgarantie.

Die Folge, die Sie erhält

Hier ist das klassische Fehlverhalten, Schritt für Schritt:

Client                    Network                   Server
  |
  |--- POST /payments ------>|                         |
  |                          |--- (delivered) -------->|
  |                          |                  processing...
  |                          |                  card charged ✓
  |<-- (connection drops) ---|                  response queued
  |
  |  [retry logic kicks in]
  |
  |--- POST /payments ------>|                         |
  |                          |--- (delivered) -------->|
  |                          |                  processing...
  |                          |                  card charged ✓ (again)
  |<------- 200 OK ----------|<------------------------|
  |
  [client sees: one charge. server did: two charges]

Der Client sah nie die erste Antwort. Aus seiner Perspektive ist die Anfrage gescheitert. Aus der Serverperspektive wurde sie zweimal erfolgreich verarbeitet. Dies ist ein klassisches Problem in verteilten Systemen — Client und Server haben sich hinsichtlich dessen, was passiert ist, unterschieden.

Das betrifft nicht nur Zahlungsverarbeitung. Es betrifft Bestellgenerierung, Benutzerregistrierung, E-Mail-Sendungen, Lagerreservierungen — alles, was „zweimal feuern“ wirklich Konsequenzen hat.

Idempotenzschlüssel: das Stripe-Muster

Stripe hat die Methode populär gemacht, die heute die meisten Zahlungs-APIs verwenden. Der Client generiert einen einzigartigen Schlüssel vor dem Versenden der Anfrage und fügt ihn als Header hinzu. Der Server verwendet diesen Schlüssel, um Doppelungen zu vermeiden.

Clientseite:

// Generate once, before any retry loop
const idempotencyKey = crypto.randomUUID();

async function chargeWithRetry(amount, retries = 3) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch('/api/payments', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey, // same key on every retry
        },
        body: JSON.stringify({ amount, currency: 'usd' }),
      });

      if (response.ok) return await response.json();

      // Don't retry on client errors (4xx)
      if (response.status < 500) throw new Error(`Client error: ${response.status}`);

    } catch (err) {
      if (attempt === retries - 1) throw err;
      await sleep(Math.pow(2, attempt) * 1000); // exponential backoff
    }
  }
}

Der Schlüssel muss generiert werden bevor in jeder Wiederholungsschleife — das ist der gesamte Punkt. Wenn Sie auf jeder Versuch einen neuen UUID generieren, haben Sie die Mechanik völlig zerstört.

Ein UUID v4 funktioniert hier gut. Wenn Sie schnell einen generieren müssen, während Sie testen, IO Tools' UUID-Generator erlaubt es Ihnen, eine Gruppe davon ohne eine Bibliothek zu laden zu produzieren.

Serverseite: Speichern der Antwort, Rückgabe bei Wiederholung

Die Aufgabe des Servers ist konzeptionell einfach: Prüfen, ob dieser Schlüssel bereits gesehen wurde; wenn ja, geben Sie die gespeicherte Antwort zurück; wenn nein, verarbeiten und speichern.

// Express + Redis
app.post('/api/payments', async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];

  if (idempotencyKey) {
    const cached = await redis.get(`idem:${idempotencyKey}`);
    if (cached) {
      const { status, body } = JSON.parse(cached);
      return res.status(status).json(body);
    }
  }

  const result = await paymentProcessor.charge(req.body);
  const responseBody = { id: result.id, status: result.status };

  if (idempotencyKey) {
    // 24-hour TTL — same window Stripe uses
    await redis.setex(
      `idem:${idempotencyKey}`,
      86400,
      JSON.stringify({ status: 200, body: responseBody })
    );
  }

  res.json(responseBody);
});

Ein paar Details, die wichtig sind:

  • Fehlern im Cache, nicht nur Erfolgen. Wenn die Gebühr fehlschlägt (Karte abgelehnt), speichern Sie auch diese Fehlerantwort. Andernfalls versucht ein Wiederholungsversuch die Gebühr erneut, obwohl sie bereits einen Fehler zurückgegeben hat — genau das, was Sie vermeiden wollen.
  • Überprüfen der Konsistenz des Körperinhalts. Stripe gibt 422 zurück, wenn der gleiche Schlüssel mit unterschiedlichen Anforderungsparametern verwendet wird. Dies erfasst Fehler, bei denen Sie versehentlich einen Schlüssel für verschiedene Operationen verwenden.
  • Behandeln von gleichzeitigen Doppelungen. Zwei Anfragen mit demselben Schlüssel, die gleichzeitig eintreffen, erfordern Koordination — verarbeiten Sie eine und geben Sie auf die andere 409 zurück, oder verwenden Sie einen verteilten Lock. Redis SETNX ist das übliche Muster hier.
  • Setzen Sie eine angemessene Gültigkeitsdauer. Nach Ablauf wird der Schlüssel recycelt und neue Anfragen mit demselben Schlüssel behandelt als frisch. Speichern Sie sie nicht für immer — sonst wird Ihr Cache überlastet.

Clientseitige Fehler, die POST zweimal ohne Wiederholung auslösen

Idempotenzschlüssel schützen vor Netzwerkschichten-Wiederholungen. Sie helfen nicht, wenn der Client zwei separate, unabhängige Anfragen auslöst. Häufige Ursachen:

  • Doppelklick beim Absenden. Der Benutzer klickt, sieht nichts, klickt erneut. Deaktivieren Sie den Button sofort beim ersten Klick — nicht nach der Antwort. Der Abstand zwischen Klick und Antwort ist genau der Zeitpunkt, an dem der zweite Klick landet.
  • React StrictMode-Doppelaufruf. React 18 Strict Mode führt in der Entwicklung Effekte zweimal aus, um Fehler aufzudecken. Wenn Sie in einem useEffect eine POST-Aktion ausführen, ohne die Bereinigung, sehen Sie doppelte Anfragen in der Entwicklung. Das passiert nicht in der Produktion, aber es kann das echte Problem verbergen.
  • Browser-Formularwiederherstellung. Absenden → Navigieren → Zurück → Vorwärts. Einige Browser zeigen „Wiederherstellen?“, andere tun es einfach. Das POST-Redirect-GET-Muster (eine Weiterleitung nach einem POST) beseitigt dies vollständig.
  • Races in Event-Handlers. Ein Klick und ein schneller Enter-Taste-Druck lösen beide den Submit-Handler vor der ersten Antwort aus und deaktivieren das Formular.
  • Mobile App Hintergrundwiederholung. iOS und Android Hintergrundabfragen oder Netzwerkschichtwiederholung können eine Anfrage erneut auslösen, die das App bereits ausgelöst hat. Generieren Sie Idempotenzschlüssel zum Zeitpunkt der Benutzerintention, speichern Sie sie lokal, wenn nötig, und löschen Sie sie erst nach bestätigter Erfolg.

Eine alternative, die erwägt: PUT an einer UUID-URL

Wenn Sie beide Enden der API kontrollieren, gibt es eine strukturell sauberere Option für die Ressourcen-Erstellung: Lassen Sie den Client die Ressourcen-ID zuweisen und verwenden Sie PUT anstatt POST.

# Instead of this (POST, not idempotent):
POST /orders
{"amount": 99.00, "items": [...]}

# Do this (PUT to client-generated UUID, idempotent by spec):
PUT /orders/7f3b9c2a-4e5d-4f8b-9a1c-2d3e4f5a6b7c
{"amount": 99.00, "items": [...]}

PUT ist idempotent nach der HTTP-Spezifikation — „Setze diese Ressource in diesen Zustand“ bedeutet, dass ein erneuter PUT keine zusätzlichen Effekte hat, sobald die Ressource existiert. Der Server behandelt die Doppelung mit INSERT ... ON CONFLICT DO NOTHING oder ähnlichem.

Dieses Muster funktioniert gut für Bestellgenerierung, Entwurfshandlung und jede Ressource, bei der die Konsistenz der ID bei Wiederholungen wichtig ist. Es funktioniert nicht, wenn eine Dritte Partei (z. B. ein Zahlungsverarbeiter) die ID zuweist oder wenn der Nebeneffekt serverseitig vor dem Wissen der ID erfolgen muss.

Was Stripe tatsächlich implementiert

Stripes Idempotenzschlüssel-Dokumentation wird vollständig gelesen. Ein paar Details, die in der Praxis wichtig sind:

  • Schlüsselformat: Jeder String bis zu 255 Zeichen, begrenzt auf Ihr Stripe-Konto. Zwei verschiedene Konten mit demselben String beeinflussen sich nicht gegenseitig.
  • 24-Stunden-Fenster: Schlüssel verfallen nach 24 Stunden. Ein Wiederholungsversuch nach Verfall wird als neuer Anfrage behandelt — nützlich zu wissen, wenn Sie lange Arbeitsabläufe erstellen.
  • Körperunterschied = 422: Gleicher Schlüssel, unterschiedliche Parameter → Stripe gibt 422 Unprocessable Entity zurück. Dies ist das richtige Verhalten; es erfasst den Fehler, wenn Sie versehentlich einen Schlüssel für verschiedene Operationen verwenden.
  • Gleichzeitige Deduplizierung: Wenn zwei Anfragen mit demselben Schlüssel gleichzeitig eintreffen, verarbeitet Stripe eine und gibt auf die andere 409 Konflikt zurück. Wiederholen Sie den 409 nach kurzer Verzögerung.
  • Gefangene Fehlschläge: Wenn die Gebühr fehlschlägt (Karte abgelehnt), speichert Stripe diesen Fehler. Wiederholung mit demselben Schlüssel gibt den gleichen Ablehnungsgrund zurück. Sie benötigen einen neuen Schlüssel, um eine andere Karte zu versuchen — was korrekt ist, weil die vorherige Versuch eine vollständige, absichtliche Operation war.

Testen von Idempotenz ohne vollständige Integrationstests

Die schnellste Methode, das Verhalten zu überprüfen, ist, den Endpunkt zweimal mit demselben Schlüssel zu erreichen und zu prüfen, ob die zweite Anfrage die abgespeicherte Antwort zurückgibt, ohne dass der Nebeneffekt erneut ausgelöst wird. IO Tools' cURL-Befehlsgenerator macht es einfach, den Anforderungsanweisung mit benutzerdefinierten Headers — einschließlich Idempotency-Key — ohne die curl-Flag-Syntax zu merken.

Zu überprüfende Szenarien:

  • Gleicher Schlüssel + gleiche Körper, innerhalb der Gültigkeitsdauer → abgespeicherter Antwort zurückgegeben, Nebeneffekt wird einmal ausgelöst
  • Gleicher Schlüssel + unterschiedlicher Körper → 422 (oder Ihr gewählter Konfliktsstatus)
  • Gleicher Schlüssel nach Ablauf der Gültigkeitsdauer → behandelt als neue Anfrage
  • Kein Schlüssel bereitgestellt → normal verarbeitet (entscheiden Sie bereits, ob Schlüssel erforderlich oder optional sind)
  • Zwei gleichzeitige Anfragen mit gleichem Schlüssel → eine verarbeitet, eine erhält 409

Die kurze Version

POST ist nicht idempotent, und der Abstand zwischen „Anfrage gesendet“ und „Antwort empfangen“ ist der Ort, an dem doppelte Nebeneffekte leben. Die Lösung ist nicht kompliziert: Generieren Sie einen UUID vor jeder Wiederholungsschleife, senden Sie ihn als Header, speichern Sie die Antwort serverseitig unter diesem Header. Die komplexen Teile sind die Details — Fehlern im Cache, gleichzeitige Doppelungen, die richtige Gültigkeitsdauer — aber das grundlegende Muster ist einfach genug, um es jedem Endpoint hinzuzufügen, der wichtig ist.

Fügen Sie Idempotenzschlüssel-Unterstützung vor dem ersten Produktionsverkehr hinzu. Nach einem Doppelzahlungsfall das Muster zu lernen ist ein deutlich schlechter Zeitpunkt.

Möchten Sie werbefrei genießen? Werde noch heute werbefrei

Erweiterungen installieren

IO-Tools zu Ihrem Lieblingsbrowser hinzufügen für sofortigen Zugriff und schnellere Suche

Zu Chrome-Erweiterung Zu Kantenerweiterung Zu Firefox-Erweiterung Zu Opera-Erweiterung

Die Anzeigetafel ist eingetroffen!

Anzeigetafel ist eine unterhaltsame Möglichkeit, Ihre Spiele zu verfolgen. Alle Daten werden in Ihrem Browser gespeichert. Weitere Funktionen folgen in Kürze!

ANZEIGE Entfernen?
ANZEIGE Entfernen?
ANZEIGE Entfernen?

Nachrichtenecke mit technischen Highlights

Beteiligen Sie sich

Helfen Sie uns, weiterhin wertvolle kostenlose Tools bereitzustellen

Kauf mir einen Kaffee
ANZEIGE Entfernen?