19. modulWebhookok és külső integrációk
Cloudflare for Devs · 19. modul

Webhookok és külső integrációk

A SaaS-od a külvilággal beszél: Stripe-eseményeket fogad, partnereknek küld értesítést, harmadik fél API-jait hívja. Ez a modul azt mutatja meg, hogyan csináld ezt Workersön megbízhatóan — aláírás-ellenőrzéssel, idempotenciával, retry-jal és sorokkal.

19.1Bejövő webhook: a helyes szerkezet

A leggyakoribb hiba, hogy a webhook-végpont mindent egyben csinál: ellenőriz, feldolgoz, DB-t ír, emailt küld — és a küldő fél közben vár. Ha bármelyik lépés lassú vagy hibázik, a partner újraküldi az eseményt, és kezdődik a duplikációs káosz. A helyes szerkezet kétfázisú:

Stripe POST /webhooks 1. fázis — gyors aláírás ellenőrzés dedup + sorba tesz → 200 OK néhány ms alatt Queue retry + DLQ 2. fázis — a valódi munka előfizetés frissítése, számla, email, harmadik fél API-k… lassú lehet, újrapróbálható A küldő fél gyors választ kap → nem küld újra. A hibázó feldolgozás a sorban próbálkozik tovább.
19/1. ábra — Kétfázisú webhook-fogadás: az endpoint csak hitelesít és sorba tesz; a munka a queue consumerben történik (7. modul).

Aláírás-ellenőrzés Workersen — a nyers body csapdája

Ez a rész az, ahol Node-ból érkezve el lehet csúszni. Az aláírás a nyers, bájtszintű törzs felett készül — ha a keretrendszered már JSON-ná alakította és újra sorosítod, az ellenőrzés hibázni fog (más lesz a szóköz, a kulcssorrend). Workersön szerencsére egyszerű a helyzet: a request.text() pontosan azt adja, ami érkezett — csak arra kell figyelni, hogy előbb a szöveget vedd ki, és abból parse-olj, ne fordítva.

server/routes/webhooks/stripe.post.ts (Nuxt server route)
export default defineEventHandler(async (event) => {
  const { env, context } = event.context.cloudflare;

  // 1) NYERS törzs — ebből lesz az aláírás-ellenőrzés alapja
  const raw = await readRawBody(event, "utf8");
  const sig = getHeader(event, "stripe-signature");

  // 2) HMAC ellenőrzés Web Cryptóval (nincs natív modul, 2. modul)
  if (!(await verifyStripeSignature(raw, sig, env.STRIPE_WEBHOOK_SECRET))) {
    throw createError({ statusCode: 400, message: "invalid signature" });
  }

  const evt = JSON.parse(raw);          // csak az ellenőrzés UTÁN parse-olunk

  // 3) dedup: ugyanaz az esemény kétszer is megérkezhet
  const inserted = await env.DB.prepare(
    "INSERT OR IGNORE INTO webhook_events (id, provider, received_at) VALUES (?, 'stripe', ?)"
  ).bind(evt.id, new Date().toISOString()).run();

  if (inserted.meta.changes === 0) return { ok: true, duplicate: true };

  // 4) sorba tesszük — a valódi munka a consumerben (7. modul)
  await env.JOBS.send({ type: "stripe-event", eventId: evt.id, payload: evt });

  return { ok: true };      // gyors 200 → a partner nem küldi újra
});
server/utils/stripe-sig.ts — az ellenőrzés Web Cryptóval
export async function verifyStripeSignature(raw, header, secret) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const t = parts.t, v1 = parts.v1;

  // replay-védelem: a túl régi aláírást ne fogadjuk el
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);

  const mac = await crypto.subtle.sign(
    "HMAC", key, new TextEncoder().encode(`${t}.${raw}`));

  return timingSafeEqual(toHex(mac), v1);   // időállandó összehasonlítás!
}
Miért nem a hivatalos SDK-t használjuk? Használhatod — a modern Stripe-SDK-nak van Web Crypto-alapú, aszinkron ellenőrzője, ami Workersön is működik (a Node-változat natív crypto-t feltételez). A lényeg: ha SDK-t használsz, a webhook-ellenőrzésre mindig az aszinkron/edge-változatot hívd, és a nyers body-t add át neki. A fenti kód azért van itt, hogy lásd, mi történik a motorháztető alatt — és mert egy vékony saját implementáció kevesebb bundle-méretet eszik (2. modul: 10 MB).

Miért ne a waitUntil-t használd a feldolgozásra?

Kísértő megoldás: gyors 200, a munka pedig ctx.waitUntil()-ben. Ez kis dolgokra (log, metrika) rendben van, de webhookra nem: ha a háttérfeldolgozás hibázik, nincs retry — a partner már 200-at kapott, nem küldi újra, az esemény pedig elveszett. A Queue épp ezt a garanciát adja: retry, késleltetés, DLQ (7. modul).

19.2Idempotencia — a hálózat mindig kétszer csenget

Két oka is van, hogy ugyanaz az esemény többször feldolgozásra kerüljön: a partner újraküldi (mert nem kapott időben választ), és a Queue is legalább-egyszeri kézbesítésű (7. modul). Ezért a feldolgozásnak idempotensnek kell lennie — háromszintű védelem:

SzintEszközMit fog meg
1. BeérkezésINSERT OR IGNORE az esemény-azonosítóval (fent)a partner ismétlését
2. Feldolgozásállapot-ellenőrzés: „ez a számla már ki van fizetve?"a queue-duplikátumot
3. Kimenő hatásidempotencia-kulcs a partner API-hívásban (a legtöbb fizetési API támogatja)a dupla terhelést/emailt
a consumer oldalán
async queue(batch, env) {
  for (const msg of batch.messages) {
    try {
      const { eventId, payload } = msg.body;

      // 2. szint: az állapot dönt, nem az esemény érkezése
      const already = await env.DB.prepare(
        "SELECT processed_at FROM webhook_events WHERE id = ?"
      ).bind(eventId).first();
      if (already?.processed_at) { msg.ack(); continue; }

      await handleStripeEvent(env, payload);

      await env.DB.prepare(
        "UPDATE webhook_events SET processed_at = ? WHERE id = ?"
      ).bind(new Date().toISOString(), eventId).run();

      msg.ack();
    } catch (err) {
      msg.retry({ delaySeconds: 60 });     // hibánál újra, DLQ-ig (7. modul)
    }
  }
}
Sorrendiség: a Queues nem FIFO (7. modul), és a webhook-szolgáltatók sem garantálják a sorrendet. Ha egy előfizetés-frissítés és egy lemondás fordított sorrendben érkezik, rossz állapotba kerülhetsz. Két védekezés: ① az esemény időbélyege alapján dobd el a régebbi frissítést (WHERE updated_at < ?); ② vagy — ami sokszor egyszerűbb és biztosabb — a webhookot csak jelzésnek tekintsd, és az esemény hatására kérdezd le a partner API-jából a friss állapotot. A Stripe-nál például: ne az eseményben lévő adatot írd be, hanem kérdezd le az előfizetés aktuális állapotát, és azt mentsd.

19.3Kimenő webhookok: te vagy a küldő

Ha az ügyfeleid webhookokat kapnak tőled (ez B2B SaaS-nál elvárás), a felelősség megfordul. Amit a te oldaladról meg kell oldani:

KövetelményMegoldás
Megbízható kézbesítésQueue: az esemény bekerül, a consumer küld; hiba → retry exponenciális visszalépéssel, végül DLQ (7. modul)
Aláírás (hogy az ügyfél ellenőrizni tudja)HMAC-SHA256 a nyers törzsön, időbélyeggel, ügyfelenkénti titokkal — ugyanaz a séma, amit a Stripe-tól kaptál
Lassú/halott végponttimeout (AbortSignal.timeout), és sok hiba után az endpoint automatikus letiltása + értesítés
Sorrend és duplikátumeseményenként azonosító + időbélyeg a törzsben — hogy az ügyfél is tudjon dedupálni
Újraküldésadmin-felület: „küldd újra" gomb, és az utolsó N kézbesítés naplója
Tenant-izolációegy ügyfél lassú endpointja ne fogja meg a többiek kézbesítését — külön queue vagy megfelelő max_concurrency
a kimenő küldés magja
async function deliver(env, endpoint, event) {
  const body = JSON.stringify(event);
  const ts = Math.floor(Date.now() / 1000);
  const sig = await hmacHex(endpoint.secret, `${ts}.${body}`);

  const res = await fetch(endpoint.url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-MyApp-Signature": `t=${ts},v1=${sig}`,
      "X-MyApp-Event-Id": event.id,
      "X-MyApp-Delivery": crypto.randomUUID(),
    },
    body,
    signal: AbortSignal.timeout(10_000),   // ne lógjon örökké
  });

  if (!res.ok) throw new Error(`delivery failed: ${res.status}`);
}

19.4Kimenő API-hívások: partner-integrációk

Amikor te hívsz külső API-t (fizetés, számlázó, CRM), a Workers-környezet néhány szabálya érvényes:

Amit a Workers itt ad ajándékba: a kimenő fetch ugyanaz a webstandard API, amit a böngészőből ismersz — nincs axios-konfiguráció, proxy-beállítás, ügynök-kezelés. A partner-SDK-k viszont változó minőségben működnek: ha egy SDK Node-specifikus (natív crypto, http modul, stream-ek), gyakran egyszerűbb a REST API-t közvetlenül fetch-csel hívni, mint az SDK-t erőltetni. Egy vékony saját kliens ráadásul kisebb bundle-t jelent (2. modul).

19.5Tesztelés és fejlesztés

19.6Ellenőrizd magad

  1. Miért kétfázisú a helyes webhook-fogadás, és mi a szerepe a Queue-nak?
    Válasz

    Az endpoint csak hitelesít, dedupál és sorba tesz — így néhány ms alatt 200-at ad, és a partner nem küldi újra. A tényleges (lassú, hibázható) feldolgozás a queue consumerben fut, ahol van retry, késleltetés és DLQ. Így egy hibás feldolgozás nem jelent elveszett eseményt.

  2. Miért kell a nyers body az aláírás-ellenőrzéshez, és mi a helyes sorrend?
    Válasz

    Az aláírás a bájtszintű törzs felett készül; ha JSON-ná alakítod és újra sorosítod, más bájtsorozatot kapsz (szóközök, kulcssorrend), és az ellenőrzés hibázik. Helyes sorrend: nyers szöveg kiolvasása → aláírás-ellenőrzés (időállandó összehasonlítással, replay-ablakkal) → csak utána JSON-parse.

  3. Miért rossz ötlet a webhook feldolgozását ctx.waitUntil()-be tenni?
    Válasz

    Mert nincs retry: a partner már megkapta a 200-at, tehát nem küldi újra, és ha a háttérmunka hibázik, az esemény véglegesen elveszik. A waitUntil apró mellékhatásokra (log, metrika) való, nem üzleti feldolgozásra.

  4. Az előfizetés-frissítés és a lemondás fordított sorrendben érkezik. Mi a két védekezés?
    Válasz

    ① Időbélyeg-alapú védelem: a régebbi eseményt eldobod (WHERE updated_at < ?). ② Vagy — általában egyszerűbb — a webhookot csak jelzésnek tekinted, és az esemény hatására lekérdezed a partner API-jából a friss állapotot, azt mented el.

  5. Mit kell megoldanod, ha te küldesz webhookokat az ügyfeleidnek?
    Válasz

    Megbízható kézbesítés (queue + retry + DLQ), aláírás ügyfelenkénti titokkal és időbélyeggel, timeout és halott endpointok automatikus letiltása, esemény-azonosító a dedupáláshoz, újraküldés admin-felületről, és tenant-izoláció, hogy egy lassú ügyfél ne fogja meg a többiek kézbesítését.

Előző18. modul — Cache és teljesítmény Következő 20. modul — Backup, katasztrófa-helyreállítás és GDPR