Laravel & PHP5 min lezen

Betrouwbare webhookverwerking in Laravel

Een praktische Laravel-webhookarchitectuur voor signatures, duplicaten, snelle acknowledgement, queues, ordering, herstel en observability.

JDoor Jeffrey Klaassen van Oorschot

Een webhook-endpoint is geen gewone controlleractie. De afzender bepaalt wanneer hij wordt aangeroepen, deliveries kunnen terugkomen, events kunnen van volgorde wisselen en een timeout zegt niet of het werk wel of niet klaar was. Het veiligste ontwerp is een kleine ontvangstgrens gevolgd door duurzame, idempotente verwerking. Dat kan met gewone Laravel, een databasetabel en een queue.

Verifieer exact de bytes die binnenkwamen

Signatureverificatie gebeurt voordat JSON wordt vertrouwd. Providers ondertekenen meestal de ruwe request body met een timestamp. Opnieuw decoden en encoden kan whitespace, escaping of veldvolgorde veranderen. Daarom geef ik de ongewijzigde body uit $request->getContent() aan de verificatieroutine van de provider.

Het signing secret staat in beheerde configuratie en nooit in code of requestlogs. Ik respecteer de timestamp tolerance tegen replay en ondersteun secretrotatie met korte overlap wanneer de provider meerdere actieve secrets toestaat. Een ongeldige signature krijgt een generieke response en logs bevatten alleen een veilige reden en correlatiewaarde.

Houd verificatie aan de rand van de applicatie
$payload = $request->getContent();
$signature = $request->header('Stripe-Signature');

try {
    $event = Webhook::constructEvent(
        $payload,
        $signature,
        config('services.stripe.webhook_secret'),
    );
} catch (UnexpectedValueException|SignatureVerificationException) {
    return response()->json(['message' => 'Invalid webhook'], 400);
}

Sla de delivery op vóór productwerk begint

Na verificatie sla ik een webhook receipt op met provider, event-ID, type, ontvangstmoment, payload, status, pogingen en laatste veilige fout. Een unieke databaseconstraint op provider plus event-ID is de echte duplicate guard. Cache of unieke queuejobs kunnen extra werk verminderen, maar vervangen duurzame idempotency niet.

Het endpoint bevestigt snel nadat de receipt is gecommit en verwerking is ingepland. E-mail sturen, een andere API bellen of projections bijwerken binnen de request verhoogt timeout en retries. Kan de receipt niet worden opgeslagen, dan stuur ik geen success response voor een event dat mogelijk verloren gaat.

Maak duplicate delivery een normaal pad
$receipt = WebhookReceipt::firstOrCreate(
    [
        'provider' => 'stripe',
        'provider_event_id' => $event->id,
    ],
    [
        'event_type' => $event->type,
        'payload' => $event->toArray(),
        'received_at' => now(),
        'status' => 'pending',
    ],
);

if ($receipt->wasRecentlyCreated) {
    ProcessWebhook::dispatch($receipt->id)->afterCommit();
}

return response()->noContent();

Maak ook het zakelijke effect idempotent

Delivery dedupliceren sluit niet ieder foutwindow. Een worker kan een order wijzigen en crashen voordat de receipt als compleet staat. De retry ziet dezelfde pending receipt. Waar beide dezelfde database gebruiken, commit ik businesswijziging en completion marker in één transactie.

De domeinoperatie heeft ook een stabiele regel nodig. Een payment-ID kan een unieke constraint krijgen; een subscriptionevent wordt alleen toegepast wanneer versie of timestamp nieuwer is. De receipt alleen is geen goede idempotency key, want meerdere provider-events kunnen hetzelfde zakelijke effect beschrijven.

  • Gebruik databaseconstraints voor unieke businessidentiteiten
  • Commit domeineffect en receiptstatus samen
  • Gebruik provider-idempotency keys voor externe effecten
  • Bewaar veilige foutreden en volgende retrytijd
  • Geef permanent ongeldige events een zichtbare foutstatus

Ga ervan uit dat events niet op volgorde komen

Veel providers garanderen geen eventvolgorde. Een paid invoice kan arriveren voordat de lokale subscription-created-handler draait. Een state machine op arrival order verandert netwerktiming in een productbug. Wanneer het event genoeg bronstate bevat vergelijk ik versies of timestamps; anders haal ik het actuele object bij de provider op.

Actuele state ophalen kost een API-call en vraagt eigen timeout, retries en rate-limit-handling. Een misvormde payload retry ik niet eindeloos. Tijdelijke transportfouten, 5xx-responses en locktime-outs mogen met backoff terugkomen; onbekende eventtypen en gebroken invariants vragen review of een expliciete ignore-regel.

Scheid deliverysucces van verwerkingssucces

Een providerdashboard kan een 204 tonen terwijl de interne job later faalt. Ik meet beide fasen. Ontvangstmetrics dekken signaturefouten, duplicaten, responsetijd en opslagfouten. Verwerking meet pending count, oudste pending leeftijd, pogingen, foutreden, eventtype en tijd tot completion.

Herstel hoort een applicatiefunctie te zijn en geen handmatige database-edit. Een beveiligd command of adminactie kan één receipt opnieuw verwerken via dezelfde idempotente processor. Voor incidenten kunnen gefaalde receipts gecontroleerd in batches worden gereplayed. De oorspronkelijke payload blijft onveranderlijk.

Test de foutwindows

Mijn tests sturen een geldige en ongeldige signature, een gewijzigde body, hetzelfde event tweemaal, meerdere events voor één object en events in omgekeerde volgorde. Ik laat de worker na de businesswrite falen en controleer dat retry het effect niet herhaalt. Queue-tests bewijzen dat dispatch na commit gebeurt.

Die gevallen zijn waardevoller dan tien happy eventtypen. Een webhookintegratie wordt betrouwbaar wanneer duplicaten, vertraging, crashes en replays gewone inputs met voorspelbare uitkomsten zijn.

Praktische checklist

  • Verifieer signatures tegen de ruwe body
  • Sla iedere geaccepteerde delivery op onder een uniek provider-event-ID
  • Bevestig pas na duurzame opslag
  • Dispatch verwerking na commit
  • Maak domeineffecten zelfstandig idempotent
  • Verwerk afwijkende eventvolgorde bewust
  • Monitor oudste pending leeftijd en bied veilig replay

Verder lezen