Webhooky

Příjem i odesílání, ověření podpisu, retry, idempotence.

API znáš: ty se ptáš cizí služby. Webhook je obrácené API — cizí služba zavolá tebe, když se u ní něco stane. Místo abys se pořád ptal „už proběhla platba?", platební brána ti sama pošle zprávu „platba proběhla" ve chvíli, kdy se to stane. Tahle kapitola je o tom, jak webhooky správně přijímat i posílat — a hlavně jak je zabezpečit.


Pár pojmů na úvod

  • Webhook = HTTP request, který ti pošle cizí služba, když u ní nastane událost.
  • Producent webhooku = služba, která událost ohlašuje (platební brána, GitHub…).
  • Příjemce (endpoint) = tvůj endpoint, na který se webhook posílá.
  • Payload = data události v těle requestu (obvykle JSON).

Proč webhooky existují (polling vs push)

Bez webhooku bys musel cizí službu pořád dokola otravovat dotazem „už se to stalo?" (polling) — plýtvání a navíc se to dozvíš se zpožděním. Webhook to obrací: služba ti dá vědět sama, hned, jak se událost stane. Je to push místo pull.


Příjem webhooku — na co si dát pozor

Když přijímáš webhooky, řeš tyhle čtyři věci (jinak máš bezpečnostní díru nebo duplicity):

  1. Ověř podpis (signature). Kdokoli může poslat POST na tvůj veřejný endpoint a předstírat platební bránu. Producent proto payload podepíše tajným klíčem (typicky HMAC) a podpis přiloží v hlavičce. Ty si podpis přepočítáš a porovnáš — když nesedí, request zahoď. Bez ověření podpisu ti kdokoli může nahlásit falešnou „platbu". Dvě pasti, na které junior naletí:
    • Porovnávej v konstantním čase (crypto.timingSafeEqual, ne ==) — naivní porovnání řetězců prozradí podpis přes timing attack (útočník podle doby odpovědi uhádne znak po znaku).
    • Bráň se replay útoku. I platně podepsaný starý webhook lze přehrát. Proto producent do podpisu zahrnuje i timestamp a ty odmítneš požadavky starší než pár minut (přesně tak to dělá třeba Stripe).
  2. Buď idempotentní. Producent webhook při nejistotě pošle znovu (viz retry níže), takže ti stejná událost může přijít vícekrát. Každá událost má ID — podle něj duplicitu poznej a zpracuj ji jen jednou (viz Patterny).
  3. Odpověz rychle (2xx) a zpracuj na pozadí. Producent čeká na tvou odpověď a má timeout. Náročnou práci proto nedělej hned — ulož událost, vrať 200 a zpracuj ji na pozadí.
  4. Neodpovíš 2xx → producent to zkusí znovu. Když vrátíš chybu nebo neodpovíš, producent webhook po čase zopakuje (proto ta idempotence).

Odesílání webhooků — když je producentem ty

Když naopak ty oznamuješ události cizím systémům, musíš jim nabídnout totéž, co čekáš od druhých:

  • Podepisuj každý webhook tajným klíčem, ať příjemce pozná, že je opravdu od tebe.
  • Opakuj při selhání (retry s backoffem) — příjemce může být chvíli dole.
  • Dej události ID, ať příjemce pozná duplicitu.
  • Nikdy nečekej věčně — dej odeslání timeout a po pár marných pokusech to vzdej (a zaznamenej).
  • ⚠️ Pozor na SSRF. URL příjemce zadává uživatel — takže může nastavit http://169.254.169.254/ (cloud metadata) nebo interní adresu a donutit tvůj server volat dovnitř sítě. Validuj cílovou URL (jen veřejné IP, jen https) — viz SSRF v Security.
  • Negarantuj pořadí. Kvůli retry může payment.refunded dorazit dřív než payment.succeeded — řekni příjemci, ať se na pořadí nespoléhá a řídí se stavem/timestampem v payloadu.

Failure modes — jak to v praxi praská

  • Žádné ověření podpisu → kdokoli ti podvrhne falešnou událost (např. „platba proběhla").
  • Neidempotentní příjem → opakovaně doručený webhook provede akci vícekrát.
  • Pomalé zpracování v requestu → překročíš timeout producenta, ten to vyhodnotí jako chybu a pošle znovu → ještě víc duplicit.
  • Příjemce je dole → bez retry na straně producenta se událost ztratí.

🛠️ Cvičení

  1. Webhook, nebo polling? Pro „dozvědět se o dokončené platbě" porovnej webhook a polling a řekni, proč je webhook lepší.
  2. Ověř podpis. Naskicuj v pseudokódu, jak u přijatého webhooku přepočítáš HMAC podpis a porovnáš ho s hlavičkou. Co uděláš, když nesedí?
  3. Duplicitní webhook. Platební brána ti stejnou událost pošle dvakrát. Navrhni, jak podle ID události zajistíš, že platbu zpracuješ jen jednou.
  4. Rychlá odpověď. Zpracování webhooku trvá 10 sekund, producent má timeout 5 sekund. Co se stane a jak to opravíš?
  5. Posílání webhooků. Navrhuješ webhook pro své zákazníky. Vyjmenuj, co všechno musíš zařídit (podpis, retry, ID, timeout).
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Webhook, nebo polling — pointa: polling cizí službu pořád dokola otravuje („už se to stalo?"), plýtvá a dozvíš se to se zpožděním; webhook ti dá vědět sám a hned (push místo pull). Pozor: webhook je proto pro „dokončenou platbu" lepší — žádné zbytečné dotazy a žádné čekání.
  2. Ověř podpis — pointa: z payloadu si přepočítáš HMAC tajným klíčem a porovnáš ho s podpisem v hlavičce; když nesedí, request zahodíš. Pozor: porovnávej v konstantním čase (crypto.timingSafeEqual, ne ==) kvůli timing attacku a odmítni staré requesty podle timestampu (replay).
  3. Duplicitní webhook — pointa: každá událost má ID, podle něj poznáš, žes ji už zpracoval, a podruhé ji přeskočíš (idempotence). Pozor: producent posílá znovu při nejistotě, takže duplicitu musíš čekat vždycky — ulož zpracovaná ID a platbu připiš jen jednou.
  4. Rychlá odpověď — pointa: zpracování delší než timeout producent vyhodnotí jako selhání a webhook pošle znovu → duplicity; oprava je událost jen uložit, vrátit 200 a zpracovat na pozadí. Pozor: náročnou práci nikdy nedělej rovnou v requestu.
  5. Posílání webhooků — pointa: nabídni příjemci totéž, co čekáš od druhých — podepisuj tajným klíčem, opakuj při selhání (retry s backoffem), dej události ID a měj timeout + limit pokusů. Pozor: validuj cílovou URL kvůli SSRF a negarantuj pořadí (příjemce ať se řídí stavem/timestampem).

🧠 Otázky & odpovědi

Co je webhook a čím se liší od běžného volání API?

Webhook je obrácené API: místo abys ty volal cizí službu, cizí služba zavolá tebe (pošle HTTP request na tvůj endpoint), když u ní nastane událost. Běžné API je pull (ptáš se ty), webhook je push (služba ti dá vědět sama, hned). Hodí se, abys nemusel cizí službu pořád dokola pollovat „už se to stalo?".

Proč musíš u přijatého webhooku ověřit podpis?

Tvůj endpoint je veřejný, takže na něj může poslat POST kdokoli a předstírat třeba platební bránu — nahlásit ti falešnou „platbu". Producent proto payload podepíše tajným klíčem (HMAC) a podpis přiloží v hlavičce. Ty ho přepočítáš a porovnáš; když nesedí, request zahodíš. Bez ověření podpisu ti kdokoli podvrhne libovolnou událost.

Proč musí být příjem webhooku idempotentní?

Producent webhook při nejistotě (timeout, tvoje chyba) pošle znovu, takže ti stejná událost může dorazit vícekrát. Bez idempotence to znamená vícekrát provedenou akci (dvojí připsání platby). Každá událost má ID — podle něj duplicitu poznáš a zpracuješ ji jen jednou.

Proč na webhook odpovědět rychle a práci nechat na pozadí?

Producent na tvou odpověď čeká a má timeout (třeba 5 sekund). Když uděláš náročnou práci rovnou v requestu a nestihneš odpovědět včas, producent to vyhodnotí jako selhání a webhook pošle znovu — naděláš si duplicity. Lepší je událost jen uložit, vrátit 200 a zpracovat ji na pozadí.

Co musíš zařídit, když webhooky sám odesíláš?

Totéž, co čekáš od druhých: každý webhook podepiš tajným klíčem (ať příjemce pozná, že je od tebe), opakuj při selhání (příjemce může být chvíli dole), dej události ID (ať příjemce pozná duplicitu) a měj timeout + limit pokusů, ať nečekáš věčně. Selhání po vyčerpání pokusů zaznamenej.