Handlery webhooków, które przetrwają produkcję
Poradniki

Handlery webhooków, które przetrwają produkcję

Podpisy przez rotację sekretu, drabina ośmiu prób i handler, który można bezpiecznie uruchomić dwa razy - checklista webhooków cmssy.

Z
Zespół Cmssy
5 min read

Handlery webhooków, które przetrwają produkcję

Czym jest dostawa webhooka cmssy, jak zweryfikować jej podpis, co robi drabina ponowień i jak napisać handler, który można bezpiecznie uruchomić dwa razy.

Handler webhooków wygląda jak dziesięć linijek kodu - aż do pierwszego razu, gdy zaksięguje płatność dwa razy, zgubi połowę dostaw podczas rotacji sekretu albo po nieudanym deployu po cichu przestanie cokolwiek odbierać. Nic z tego nie jest egzotyczne. Tak kończy się handler, który zakłada, że dostawa przychodzi raz, po kolei i podpisana jedynym sekretem, jaki zna.

Ten wpis to checklista handlera webhooków cmssy, który przetrwa produkcję: czym jest dostawa, jak ją zweryfikować, co robi drabina ponowień i jak napisać handler, który można bezpiecznie uruchomić dwa razy.

Czym jest dostawa

Każda dostawa to POST z treścią JSON:

{
  "id": "0b6f2c1e-…",
  "event": "order.paid",
  "createdAt": "2026-10-26T14:21:19.846Z",
  "data": { "workspaceId": "…", "order": { … } }
}

id to id dostawy. Pozostaje takie samo przy każdym ponowieniu tej dostawy i przychodzi też w nagłówku x-cmssy-webhook-id, obok x-cmssy-event i x-cmssy-signature. Ponowne wysłanie z panelu admina lub przez redeliver_webhook_delivery tworzy nową dostawę z nowym id, więc handler deduplikujący po id traktuje ręczne powtórzenie jako nowe - a zwykle właśnie tego chcesz, gdy powtarzasz celowo.

Payloady są oszczędne: zdarzenia treści niosą referencje, nie treść, a form.submitted niesie id zgłoszenia, nie odpowiedzi. Zdarzenia commerce niosą migawkę zamówienia, której potrzebuje Twoja integracja - status, sumy, traktowanie podatkowe, kupującego, paczki i faktury.

Zweryfikuj podpis

Nagłówek x-cmssy-signature: t to znacznik czasu w milisekundach, v1 to HMAC-SHA256 ze znacznika czasu, kropki i surowej treści z sekretem endpointu jako kluczem, a podczas rotacji sekretu pojawia się drugie v1.
Jeden znacznik czasu, jeden podpis na każdy aktywny sekret.

Nagłówek ma postać t=<unix-ms>,v1=<hex>. Hex to HMAC-SHA256 z <t>.<raw body>, z sekretem endpointu jako kluczem. Na trzech szczegółach wykładają się ręcznie pisane weryfikatory:

  • Rotacja wysyła dwa podpisy. Podczas rotacji sekretu nagłówek niesie jedno v1 na każdy aktywny sekret. Weryfikator, który sprawdza tylko ostatnie, odrzuca każdą dostawę podpisaną drugim - przez cały czas trwania rotacji.
  • Treść musi być surowa. Sparsuj JSON i zserializuj go ponownie, a bajty się zmienią - kolejność kluczy, białe znaki, escape'y unicode. Weryfikuj await request.text(), a dopiero potem parsuj.
  • Stare znaczniki czasu muszą być odrzucane. Bez okna tolerancji przechwyconą dostawę można odtwarzać w nieskończoność.

Helper z SDK robi wszystkie trzy rzeczy: czyta każde v1, porównuje w stałym czasie, odrzuca znacznik czasu starszy niż pięć minut i zwraca typowane zdarzenie. Jest w @cmssy/next, @cmssy/remix i @cmssy/astro, a dla całej reszty w @cmssy/core.

import { verifyCmssyWebhook, CmssyWebhookError } from "@cmssy/next";

export async function POST(request: Request) {
  const body = await request.text();
  try {
    const event = await verifyCmssyWebhook({
      body,
      signatureHeader: request.headers.get("x-cmssy-signature"),
      secret: [process.env.CMSSY_WEBHOOK_SECRET, process.env.CMSSY_WEBHOOK_SECRET_PREVIOUS]
        .filter((s): s is string => !!s),
    });
    await handle(event);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof CmssyWebhookError) {
      return new Response(null, { status: 400 });
    }
    throw error;
  }
}

Opcja secret przyjmuje tablicę, więc Twój handler może trzymać nowy i poprzedni sekret w Twoim własnym oknie rotacji i akceptować dostawy podpisane którymkolwiek z nich.

Drabina ponowień

Osiem prób dostawy: pierwsza, potem plus 1 minuta, 5 minut, 30 minut, 2 godziny, 5 godzin, 10 godzin i 10 godzin, łącznie około 27 godzin.
Osiem prób w ciągu około 27 godzin.

cmssy czeka na odpowiedź 15 sekund. Wszystko poza 2xx - i każdy timeout - jest ponawiane: łącznie osiem prób, z odstępami 1 minuta, 5 minut, 30 minut, 2 godziny, 5 godzin, 10 godzin i 10 godzin. Handler, który leży całe popołudnie, nic nie traci.

  • Przekierowania to porażki. 3xx nie jest śledzone. Zarejestruj docelowy URL.
  • Tylko 410 Gone zatrzymuje drabinę. Każde inne 4xx jest ponawiane, bo „ten endpoint skończył się na dobre” i „deploy, do którego należy ta trasa, przez chwilę odpowiada 404” z zewnątrz wyglądają tak samo. Zwracaj 410, gdy naprawdę o to chodzi.
  • Retry-After jest respektowany, gdy prosi o dłuższe czekanie niż drabina, maksymalnie do 10 godzin. Nigdy nie skraca odstępu.
  • Dwadzieścia porażek z rzędu wyłącza endpoint. Napraw handler, a potem włącz go ponownie; to zeruje licznik.

Dostawy są kolejkowane per endpoint, z jedną próbą w toku naraz, więc wolny handler kolejkuje własne ponowienia, zamiast być zasypywany przez fan-out.

Napisz handler, który można bezpiecznie uruchomić dwa razy

Dostawa at-least-once oznacza, że Twój handler prędzej czy później zobaczy tę samą dostawę dwa razy - timeout po Twojej stronie, który cmssy policzyło jako porażkę, deploy, który odpowiedział 502 już po wykonaniu pracy. A ponieważ dostawy nie są uporządkowane, ponowienie może dotrzeć po nowszym zdarzeniu dotyczącym tego samego zamówienia.

  • Potwierdzaj szybko, pracuj potem. Zweryfikuj, zapisz dostawę, zwróć 2xx. Handler, który przed odpowiedzią woła trzy API, w końcu przekroczy 15 sekund i zostanie ponowiony, gdy wciąż pracuje.
  • Deduplikuj po id dostawy. Unikalny indeks na id w Twojej własnej tabeli zamienia duplikat w no-op.
  • Przed efektami ubocznymi czytaj aktualny stan. Payload to migawka z chwili zdarzenia. Gdy to ma znaczenie - wystawienie faktury, wysyłka e-maila, przesunięcie stanów w innym systemie - odczytaj zamówienie ponownie i działaj na tym, co jest prawdą teraz.
  • Nigdy nie opieraj się na kolejności przyjścia. Porównuj stan z payloadu albo ze świeżego odczytu z tym, co już zastosowałeś.

Gdy coś jednak pójdzie nie tak

Każda próba jest logowana: list_webhook_deliveries (lub Settings → Webhooks w panelu admina) pokazuje status, kod odpowiedzi, a członkom zespołu z webhooks:manage pierwsze 8 KiB treści odpowiedzi Twojego handlera - zwykle ten stack trace, którego szukałeś. Gdy handler jest naprawiony, wyślij ponownie nieudane dostawy. Ponowne wysłanie to świeża dostawa z własnym id; nie liczy się do drabiny oryginału ani do serii porażek endpointu.

Jedna subskrypcja czy wiele?

Do unieważniania cache zasubskrybuj raz content.changed i unieważniaj przy każdej zmianie; obejmuje strony, rekordy, modele, formularze, media i ustawienia. Do automatyzacji subskrybuj szczegółowe zdarzenia, których potrzebujesz - order.*, shipment.*, page.published, form.submitted i resztę. list_webhook_event_types zwraca pełną listę dozwolonych zdarzeń, przefiltrowaną według tego, co może subskrybować Twoja rola.

Pełna dokumentacja jest w dokumentacji webhooków. Działający przykład idempotentnego handlera znajdziesz we wpisie fakturowanie headless sklepu z webhookami.