Warum derselbe Webhook zweimal laufen kann
Anbieter wiederholen Zustellungen, daher kann dein Empfänger dasselbe Ereignis mehr als einmal sehen. Stripe schreibt das in der Dokumentation und empfiehlt, die bereits verarbeiteten Ereignis-IDs zu protokollieren (Stripe: doppelte Ereignisse). Wenn dein Endpoint zu lange braucht oder einen Fehler zurückgibt, sendet der Anbieter das Ereignis erneut, und Stripe wiederholt eine Zustellung im Live-Modus bis zu drei Tage lang mit exponentiellem Backoff (Stripe: automatische Wiederholungen). Wenn sich nichts merkt, dass das Ereignis schon verarbeitet wurde, läuft der Auftrag dahinter noch einmal: eine zweite Abbuchung, eine zweite Bestellung, eine zweite CRM-Zeile. In einem Shop kann ein Webhook, der zweimal läuft, für eine Bestellung zwei Sendungen oder zwei Gutscheine auslösen.
Ein stabiler Schlüssel, nur einmal beansprucht
Die Lösung ist ein Schlüssel, der über alle Wiederholungen gleich bleibt und nur einmal beansprucht wird. Jede Stripe-Wiederholung trägt eine neue Signatur und einen neuen Zeitstempel, daher kann der Header stripe-signature nicht der Schlüssel sein. Die Ereignis-ID schon: Die Vorlage nutzt evt: plus die ID aus dem Körper, und wenn der Körper keine ID hat, sha256: plus den SHA-256 des rohen Körpers. Der Claim ist eine einzige Anweisung auf einem Primärschlüssel, INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, daher bekommt nur eine Zustellung die Zeile zurück, auch wenn zwei gleichzeitig ankommen. Kommt eine Zeile zurück, läuft der Geschäftsschritt. Kommt keine zurück, wurde das Ereignis schon gesehen: Der Empfänger antwortet trotzdem mit 200, und Abbuchung, Bestellung oder CRM-Zeile entstehen kein zweites Mal. Die Reihenfolge der Prüfungen ist auch für die Sicherheit wichtig. Prüfe zuerst die Signatur des Anbieters (Stripe: Ereignisse prüfen) und beanspruche erst dann den Schlüssel, damit eine gefälschte Anfrage keine echte Ereignis-ID beanspruchen kann. Die Vorlage prüft keine Signaturen, diese Prüfung gehört also davor.
Was passiert ist, als wir es ausgeführt haben
Wir haben die n8n-Vorlage unten auf unserer eigenen Maschine gegen Postgres ausgeführt, mit n8n 2.42.6 und Postgres 16. Wir haben Ereignis A gesendet, dann Ereignis A noch einmal mit einem anderen Signatur-Header, dann Ereignis B. n8n hat 3 Ausführungen aufgezeichnet und die Geschäftstabelle hat 2 Zeilen bekommen. Die Wiederholung von A bekam eine 200-Antwort und hat den Geschäftsschritt nicht ausgeführt, die Wiederholung wurde also beantwortet und nichts wurde doppelt geschrieben. Danach haben wir zweimal einen Körper ohne ID gesendet: Der Schlüssel war der SHA-256 des rohen Körpers, und der zweite Aufruf nahm den Duplikat-Zweig. In einer separaten Prüfung haben wir mit dem SQL, das diese Seite erzeugt, 8 gleichzeitige Claims für denselben Schlüssel an Postgres geschickt: Einer bekam die Zeile zurück, und der Geschäftsschritt lief einmal.
So kommt es in n8n
Wer n8n betreibt, kann den Workflow importieren, den wir nach diesem Lauf exportiert haben, ohne die Credential-ID aus dem Labor, ihn mit Postgres verbinden und den Geschäftsschritt durch eigene Nodes ersetzen. Der Webhook-Node nutzt den Antwortmodus "Using Respond to Webhook node", daher antwortet der Zweig, der gelaufen ist. Der Code-Node baut den Schlüssel, ein Postgres-Node führt den Claim aus, und "Row Returned?" schickt ein neues Ereignis zum Geschäftsschritt und eine Wiederholung zu "Respond 200 duplicate".
n8n-Workflow-Vorlage (Webhook, Code, Postgres, IF, Respond to Webhook)
Der exportierte Workflow als .json-Datei, bereit zum Import.
-
Tabellen anlegen
CREATE TABLE IF NOT EXISTS processed_events ( key text PRIMARY KEY, first_seen timestamptz NOT NULL DEFAULT now() ); -- Beispiel-Geschäftstabelle der Vorlage. Ersetze sie durch deinen eigenen Schritt. CREATE TABLE IF NOT EXISTS business_log ( id bigserial PRIMARY KEY, event_key text NOT NULL, event_type text, created_at timestamptz NOT NULL DEFAULT now() );
-
Umgebung und Zugangsdaten setzen
Selbst gehostetes n8n blockiert eingebaute Module im Code-Node standardmäßig. Setze
NODE_FUNCTION_ALLOW_BUILTIN=cryptoim n8n-Container, damitrequire('crypto')funktioniert. Öffne nach dem Import beide Postgres-Nodes und wähle deine Postgres-Credential (Host, Port, Datenbank, Benutzer, Passwort). n8n kann je nach deinen Ausführungseinstellungen auch Ausführungsdaten speichern, einschließlich des Webhook-Körpers. Wenn der Körper personenbezogene Daten enthält, prüfe diese Einstellungen und ihre Aufbewahrungsdauer. -
Importieren, eigenen Schritt anschließen, veröffentlichen
Importiere in n8n die .json-Datei. Ersetze "Business Step (insert business_log)" durch deine eigenen Nodes, lass sie auf dem true-Zweig von "Row Returned?" und veröffentliche den Workflow. Setze deine Signaturprüfung vor den Code-Node, wenn dein Anbieter Anfragen signiert.
-
Mit einer wiederholten Zustellung testen
Sende dasselbe Ereignis zweimal und ein anderes Ereignis einmal. Unter Executions sollten der erste und der dritte Lauf durch den Geschäftsschritt gehen und der zweite "Respond 200 duplicate" nehmen.
-
Alte Schlüssel aufräumen
Das Aufräumen ist ein eigener Schritt außerhalb des Webhook-Workflows. Halte Schlüssel länger, als der Anbieter erneut senden kann. Stripe erlaubt ein manuelles erneutes Senden aus dem Dashboard bis zu 15 Tage lang und über die CLI bis zu 30 Tage lang (Stripe: manuelle Wiederholungen). Ein geplanter n8n-Workflow oder ein Cron-Job kann Folgendes ausführen:
DELETE FROM processed_events WHERE first_seen < now() - interval '30 days';
Grenze: Der Schlüssel wird beansprucht, bevor der Geschäftsschritt läuft. Schlägt der Geschäftsschritt nach dem Claim fehl, wird eine Wiederholung dieses Ereignisses als Duplikat behandelt. Füge einen Fehlerpfad hinzu, der DELETE FROM processed_events WHERE key = $1 für diesen Schlüssel ausführt, oder behebe den Fehler und spiele das Ereignis von Hand erneut ein.
Was gespeichert wird und was in deinem Browser bleibt
Der Schlüssel auf dieser Seite wird mit crypto.subtle.digest in deinem Browser berechnet, und das Seitenskript stellt mit dem, was du einfügst, keine Netzwerkanfrage. In Postgres speichert processed_events den Schlüssel und einen Zeitstempel. Der Schlüssel ist die Ereignis-ID des Anbieters oder ein SHA-256 des Körpers, wenn keine ID vorhanden ist. Die Beispieltabelle business_log speichert den Schlüssel und den Ereignistyp, und dein eigener Geschäftsschritt entscheidet, was sonst noch gespeichert wird. Der Planer oben erzeugt denselben Claim wie die n8n-Vorlage: INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, einen Zweig nach der zurückgegebenen Zeile und ein separates Aufräum-DELETE. Diese Hinweise beschreiben den Code auf dieser Seite und in der Vorlage. Sie sind keine Zertifizierung.