Zum Inhalt springen

Zahlungs-Webhooks kommen spät und doppelt

Die Rückleitung von der Bezahlseite ist keine Bestätigung. Ein einzelner Webhook auch nicht. Was ein Zahlungsanbieter über Zustellung wirklich zusagt, und wie man darauf etwas Korrektes baut.

5 Min. Lesezeit

Ein Zahlungsanbieter sagt Ihnen, dass er Ereignisse zuverlässig zustellt. Das stimmt, und es ist eine engere Zusage, als die meisten Teams darin lesen. Es heißt: Das Ereignis kommt irgendwann an, wahrscheinlich mehr als einmal, möglicherweise Stunden nach dem, was es beschreibt, und nicht zwingend in der Reihenfolge, in der die Dinge passiert sind.

Jede Entwurfsentscheidung unten folgt daraus, diesen Satz wörtlich zu nehmen.

Die Rückleitung ist Oberfläche, kein Ergebnis

Wenn der Kunde von der Bezahlseite zurückkommt, erhält Ihre Anwendung eine Anfrage, die im Kern sagt: Der Kunde ist wieder da. Sie sagt nicht, dass die Zahlung erfolgreich war, und sie wird nicht vom Anbieter geschickt - sie kommt vom Browser des Kunden, und der kann geschlossen, neu geladen oder von einem Handy ersetzt werden, dem im Aufzug der Akku ausgeht.

Nutzen Sie die Rückleitung für genau eine Aufgabe: einen Bildschirm zeigen. Lesen Sie den aktuellen Zustand der Zahlung beim Anbieter, zeigen Sie, was Sie wissen, und sagen Sie es, wenn Sie es noch nicht wissen. Was die Rückleitung niemals tun darf, ist "bezahlt" in Ihre Datenbank schreiben.

Bestätigt wird die Zahlung durch das Ereignis, und das Ereignis kommt über einen Kanal, auf den der Kunde keinen Einfluss hat.

Prüfen Sie die Bytes, nicht das Objekt

Die Signatur ist ein Hash über den rohen Anfragekörper mit einem geteilten Geheimnis, plus einem Zeitstempel, damit eine alte Zustellung nicht wiedereingespielt werden kann. Zwei Dinge gehen schief, und beide sehen aus wie ein falscher Schlüssel:

public function handle(Request $request): Response
{
    $payload = $request->getContent();   // roh, nicht $request->all()
 
    try {
        $event = Webhook::constructEvent(
            $payload,
            $request->header('Stripe-Signature'),
            config('services.stripe.webhook_secret'),
        );
    } catch (SignatureVerificationException) {
        return response()->noContent(400);
    }
 
    ProcessPaymentEvent::dispatch($event->id, $payload);
 
    return response()->noContent(200);
}

Das erste ist Parsen vor dem Hashen. $request->all() gibt Ihnen ein Array, und dessen erneute Kodierung erzeugt andere Bytes als angekommen sind - andere Schlüsselreihenfolge, anderes Unicode-Escaping, andere Zahlenformatierung. Hashen Sie die Zeichenkette.

Das zweite ist die CSRF-Middleware, die die Anfrage verwirft, bevor irgendetwas davon läuft, denn ein Zahlungsanbieter hat keine Session und kein Token. Die Route gehört außerhalb der Web-Middleware-Gruppe, und liegt sie darin, ist der Fehler eine 419, die der Anbieter drei Tage lang wiederholt.

Beachten Sie, was der Handler nach der Prüfung tut: fast nichts. Er übergibt die Arbeit an eine Queue und kehrt zurück. Anbieter erwarten eine schnelle Bestätigung, und die Arbeit inline zu erledigen macht aus einer langsamen Abfrage einen Wiederholungssturm.

Falsche Reihenfolge ist der Normalfall

Zwei Ereignisse zu einer Zahlung können in falscher Folge ankommen. Eine Belastung gelingt und wird neunzig Sekunden später erstattet; das Erstattungsereignis überholt das Erfolgsereignis; Ihr Handler verarbeitet die Erstattung gegen eine Bestellung, die noch nicht bezahlt ist, hält das für unsinnig und tut nichts. Jetzt ist die Bestellung für immer bezahlt.

Beheben Sie das nicht über Reihenfolge. Beheben Sie es, indem jeder Handler die Welt beschreibt statt einen Übergang:

  • Falsch: "bei Erstattung, setze Status von eingezogen auf erstattet."
  • Richtig: "bei jedem Ereignis zu dieser Zahlung, hole ihren aktuellen Zustand beim Anbieter und gleiche meine Zeile daran an."

Das Ereignis wird zum Signal nachzusehen, und seine Ankunftsreihenfolge hört auf zu zählen. Es kostet einen API-Aufruf pro Ereignis und entfernt eine ganze Fehlerklasse, die sonst in Produktion von einem verwirrten Buchhalter gefunden wird.

Wo Sie wirklich nicht nachladen können, speichern Sie den Ereigniszeitstempel des Anbieters an der Zeile und ignorieren alles, was älter ist als das bereits Angewandte.

Doppelt ist ebenfalls der Normalfall

Jeder Anbieter wiederholt bei allem, was keine 2xx ist, und eine Zustellung, die nach erfolgreichem Code in einen Timeout lief, wird auch wiederholt. Dasselbe Ereignis landet zweimal.

Speichern Sie die Ereignis-ID des Anbieters mit einem eindeutigen Index, fügen Sie sie vor der Verarbeitung ein, und lassen Sie die Datenbank das Duplikat ablehnen. Das ist der ganze Mechanismus, und er ist verlässlicher als zu prüfen, ob die Zeile schon existiert, denn eine Prüfung gefolgt von einem Insert ist ein Wettlauf zweier Worker, und bei Wiederholungsstürmen sind beide Worker real.

Und dann trauen Sie dem Kanal gar nicht mehr

Alles oben macht den Webhook-Pfad korrekt. Es macht ihn nicht vollständig, denn ein Endpunkt, der während eines Deploys vier Stunden weg war, hat Ereignisse verpasst, und nach Ablauf des Wiederholungsfensters sind sie fort.

Lassen Sie also einen Abgleichsjob laufen. Fragen Sie den Anbieter einmal täglich nach jeder Zahlung, die sich seit Ihrem letzten erfolgreichen Lauf geändert hat, und vergleichen Sie sie mit Ihren Zeilen. Melden Sie die Unterschiede, statt sie still zu korrigieren - eine Abweichung bedeutet meist einen Fehler, und ein Job, der Ihre Daten leise repariert, versteckt diesen Fehler ein Jahr lang.

Das ist dieselbe Form wie bei den Buchhaltungsanbindungen, die wir bauen: ein Ereignisstrom für Aktualität, ein periodischer Abruf für Richtigkeit, und der Abruf ist das, worauf Sie sich verlassen können. Bei Zahlungen gibt es einen zweiten Grund dafür: Irgendwann fragt jemand aus der Buchhaltung, warum die Monatssumme des Anbieters und Ihre Datenbank auseinandergehen, und "tun sie nicht" ist eine deutlich bessere Antwort als eine Untersuchung.

Wenn Sie das zusammen mit dem Bestellmodell aufsetzen, entscheidet wie die Zahlung modelliert ist die Hälfte dessen, was diese Handler tun müssen, und das klärt man besser zuerst.

Verwandte Fragen

Reicht die Rückleitung wirklich nicht?
Nein, und der Grund ist, dass sie vom Browser des Kunden abhängt. Er schließt den Tab, das Handy verliert im Zug das Netz, die Authentifizierungsseite der Bank führt ihn woanders hin, oder er wartet schlicht nicht ab. Die Zahlung geht trotzdem durch. Markiert der Rückleitungs-Handler die Bestellung als bezahlt, ist jeder dieser Fälle abgebuchtes Geld ohne zugehörige Bestellung.
Warum scheitert unsere Signaturprüfung, obwohl die Payload stimmt?
Fast immer, weil etwas die Anfrage vor dem Hashen geparst hat. Die Signatur wird über genau die gesendeten Bytes gebildet; ein Framework, das JSON dekodiert und neu kodiert, oder ein Proxy, der den Body umformatiert, erzeugt eine andere Zeichenkette und damit einen anderen Hash. Lesen Sie den Rohtext, prüfen Sie den, und parsen Sie danach.
Müssen wir jeden Ereignistyp behandeln?
Nein, und alles zu abonnieren ist der Weg, den Endpunkt langsam und laut zu machen. Abonnieren Sie die wenigen, die Ihren Zustand ändern - erfolgreich, fehlgeschlagen, erstattet, angefochten, plus was Ihr Abo-Lebenszyklus braucht - und ignorieren Sie den Rest ausdrücklich statt versehentlich. Bestätigen Sie auch die ignorierten mit einer 200.
Wie lange heben wir die Rohereignisse auf?
Länger, als sich vernünftig anfühlt. Sie sind klein und sind der einzige Nachweis darüber, was der Anbieter Ihnen wann gesagt hat. Wenn ein Kunde eine acht Monate alte Belastung anficht oder die Buchhaltung einen Monat nicht abgleichen kann, ist diese Tabelle das, was antwortet. Ein Jahr davon kostet fast nichts.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite