Changelogi webhooków: zmiana łamiąca bez prośby
5 min czytania
Changelog API REST istnieje, bo wywołujący może odrzucić odpowiedź, której nie rozumie, albo przynajmniej zalogować błąd na tyle głośno, żeby ktoś to zauważył. Odbiorca webhooka rzadko robi jedno albo drugie. Dostaje POST, czyta oczekiwane pola, a jeśli pole się przesunęło, zmieniło typ albo zniknęło, endpoint albo pada po cichu wewnątrz zadania w tle, którego nikt nie obserwuje, albo, gorzej, działa dalej z błędną wartością, której nigdy nie zwalidował. Czym jest zmiana łamiąca opisuje ogólną definicję; payload webhooka potrzebuje własnej odpowiedzi, bo tryb awarii jest inny niż w przypadku endpointu, który ktoś wywołuje celowo.
Dlaczego zmiana payloadu webhooka psuje się inaczej niż zmiana odpowiedzi API?
Bo kierunek żądania jest odwrócony. Wywołujący REST inicjuje wywołanie i może dodać nagłówek wersji, ponowić przy 4xx, albo przeczytać ostrzeżenie o wycofaniu w odpowiedzi. Odbiorca webhooka nie zainicjował niczego z tego: to wasz serwer zdecydował o wysłaniu, zdecydował kiedy i zdecydował, jaki kształt będzie miało body. Jedyną dźwignią odbiorcy jest walidacja, którą napisał podczas budowania integracji, a większość integracji buduje się raz, działają, i nikt do nich nie wraca, dopóki się nie zepsują. Ta asymetria to cały powód, dla którego zmiana payloadu webhooka zasługuje na więcej ostrożności niż taka sama zmiana w body odpowiedzi, o którą wywołujący aktywnie prosił.
Co naprawdę liczy się jako zmiana łamiąca w payloadzie webhooka?
| Zmiana | Łamiąca dla większości odbiorców |
|---|---|
| Dodanie nowego pola | Nie, jeśli odbiorcy ignorują nieznane pola (zweryfikujcie to założenie, nie zakładajcie) |
| Usunięcie pola | Tak, jeśli cokolwiek je czyta |
| Zmiana nazwy pola | Tak, funkcjonalnie identyczne z usunięciem starego |
| Zmiana typu pola (string na obiekt) | Tak, prawie zawsze |
| Zmiana kolejności pól w body JSON | Nie, dla każdego odbiorcy parsującego po kluczu, a wszyscy powinni |
| Zmiana nazwy lub typu zdarzenia | Tak, jeśli odbiorcy filtrują lub routują na tej podstawie |
Wiersz “dodanie pola jest bezpieczne” to ten, na którym zespoły polegają najbardziej i ten, który najbardziej opłaca się zweryfikować, a nie założyć. Permisywny parser JSON domyślnie ignoruje nieznane pola, ale odbiorca deserializujący do ścisłego schematu, kilka języków typowanych robi to bez dodatkowej konfiguracji, może odrzucić cały payload, gdy tylko pojawi się nieoczekiwane pole. Dodanie pola jest bezpieczne dla waszego webhooka tylko wtedy, gdy wiecie, jak odbiorcy parsują, a nie dlatego, że sam JSON jest permisywny.
Jak nadać wersję payloadowi webhooka?
Podobnie jak przy odpowiedzi API, z jedną różnicą: odbiorca nigdy nie wysyła żądania, więc nie może
poprosić o wersję, i to nadawca musi ją podać. Może trafić do body albo do nagłówka żądania samej
dostawy; dostawy GitHuba
niosą X-GitHub-Event i X-GitHub-Hook-ID, a
specyfikacja Standard Webhooks
umieszcza swoje metadane w nagłówkach webhook-*. Pole wersji w
payloadzie ("payload_version": 2) to najtańsza opcja i działa, gdy odbiorcy są gotowi na jej
podstawie się rozgałęziać. Wersjonowany typ zdarzenia (invoice.updated staje się
invoice.updated.v2 jako odrębne zdarzenie, na które odbiorca dobrowolnie się zapisuje) wymaga
więcej pracy przy budowie, ale oznacza, że stary kształt nadal płynie do tych, którzy nigdy nie
zmigrowali, co liczy się tu bardziej niż w endpoincie REST, bo nie możecie zadzwonić do każdego
odbiorcy z prośbą o aktualizację. Ustawienie na subskrypcję, wybrane przy rejestracji endpointu
webhooka, wysuwa decyzję do przodu zamiast rozgałęziać przy każdej dostawie, i jest właściwym
wyborem, gdy macie już rekord subskrypcji, do którego można ją dołączyć.
POST /endpoint-odbiorcy
{
"event": "invoice.updated",
"payload_version": 2,
"data": { "invoice_id": "inv_123", "status": "paid" }
}
Skąd w ogóle wiecie, kto słucha?
Gorzej niż odpowiednik tego problemu w changelogu API, bo webhook nie ma po waszej stronie logu przychodzących żądań, który nazwałby wywołującego; macie tylko własny log wychodzących dostaw, który mówi, że endpoint dostał 200, nie co zrobił z body. Śledźcie co najmniej dwie rzeczy: każdy zarejestrowany endpoint z właścicielem, tę samą dyscyplinę, którą changelogi API wewnętrznego zalecają dla konsumentów wewnętrznych, i wasz wskaźnik nieudanych dostaw na endpoint po zmianie payloadu. Skok odpowiedzi 4xx lub 5xx z endpointu zaraz po zmianie to najbliższe temu, co dostaniecie zamiast stack trace, i często jedyny sygnał, że odbiorca się zepsuł, bo zespół, który go obsługuje, może tego nie zauważyć przez wiele dni.
Czy changelog webhooków powinien być osobny od changelogu API?
Osobna sekcja na tej samej stronie, nie osobna publikacja. Changelog API już ustala, kto go czyta i jak się subskrybuje; zmiana payloadu webhooka należy do tego samego strumienia, oznaczona wystarczająco wyraźnie, żeby developerka po stronie odbiorcy skanująca “czy to wpływa na moją integrację” mogła to przefiltrować, bo konsumentka webhooka często nie ma innego powodu, żeby sprawdzać ogólny changelog API, i znajdzie go tylko wtedy, gdy ktoś ją tam bezpośrednio skieruje.
Jak powinno wyglądać rozsądne okno wycofania dla payloadu webhooka?
Dłuższe niż odpowiednie wycofanie REST, bo migracja po stronie odbiorcy zwykle oznacza, że drugi
zespół, z którym możecie nie mieć bezpośredniego kontaktu, musi to zauważyć, zaplanować i wydać
bez własnej pilności. Miesiąc to rozsądne minimum dla pola, które odbiorca prawdopodobnie nadal
parsuje permisywną biblioteką; trzy miesiące lub więcej są bezpieczniejsze przy usunięciu pola,
które ścisły schemat całkowicie by odrzucił. Wysyłajcie stary i nowy kształt razem podczas okna,
gdy to wykonalne (stare pole status i jego zamiennik z wersji 2 w tym samym
payloadzie), bo odbiorca czytający stare pole działa dalej bez dotykania kodu, a ten, który już
zmigrował, po prostu ignoruje pole, którego już nie potrzebuje.
FAQ
Czy konsumenci webhooków muszą potwierdzić zmianę payloadu przed jej wdrożeniem? Domyślnie nie istnieje mechanizm potwierdzenia, i właśnie dlatego okno wycofania liczy się tu bardziej niż w API REST: nikt nie potwierdza gotowości, więc okno musi być wystarczająco długie, by większość odbiorców zmigrowała we własnym tempie, zanim stary kształt zniknie.
Czy kiedykolwiek bezpiecznie jest dodać nieznane pola bez uprzedzenia? Tylko po zweryfikowaniu, nie założeniu, że wasi odbiorcy parsują permisywnie. Wpis w changelogu kosztuje niewiele i usuwa niepewność; ciche dodawanie pól przy założeniu, że “parsery JSON ignorują dodatki”, psuje każdego odbiorcę ze ścisłą deserializacją.
Jaki jest najszybszy sposób na wykrycie zepsutego odbiorcy webhooka po zmianie payloadu? Wskaźnik nieudanych dostaw na endpoint, obserwowany w godzinach zaraz po zmianie. Nie powie wam, co się zepsuło, tylko że coś się zepsuło, ale to najwcześniejszy i często jedyny sygnał, jaki dostaniecie.
Czy logika ponawiania pomaga odbiorcom przetrwać zmianę payloadu? Nie. Ponowienie wysyła ten sam nowy payload jeszcze raz; nie wraca do kształtu, który odbiorca może sparsować. Zmiana payloadu psuje odbiorcę przy pierwszej dostawie i każdym kolejnym ponowieniu identycznie.
Twierdzenia techniczne w tym artykule nie zostały niezależnie zweryfikowane. Jeśli coś się nie zgadza, daj nam znać, a poprawimy to.