Modificări de API

Changelog-uri de webhook: schimbarea necerută

6 min de citit

Un changelog de API REST există pentru că un apelant poate alege să respingă un răspuns pe care nu îl înțelege, sau cel puțin să logheze o eroare suficient de zgomotoasă încât cineva să o observe. Un receptor de webhook rareori face oricare din astea. Primește un POST, citește câmpurile pe care le așteaptă, iar dacă un câmp s-a mutat, și-a schimbat tipul sau a dispărut, endpoint-ul fie pică în tăcere într-un job de fundal pe care nimeni nu-l urmărește, fie, mai rău, continuă să ruleze cu o valoare greșită pe care n-a validat-o niciodată. Ce este o schimbare care rupe compatibilitatea acoperă definiția generală; un payload de webhook are nevoie de propriul răspuns, pentru că modul de eșec e diferit de cel al unui endpoint pe care cineva îl apelează intenționat.

De ce o schimbare de payload la webhook se strică diferit de o schimbare la răspunsul unui API?

Pentru că direcția cererii e inversată. Un apelant REST inițiază apelul și poate adăuga un header de versiune, poate reîncerca la un 4xx, sau poate citi un avertisment de deprecare în răspuns. Un receptor de webhook nu a inițiat nimic din toate astea: serverul vostru a decis să trimită, a decis când, și a decis ce formă va avea body-ul. Singura pârghie a receptorului e validarea pe care a scris-o când a fost construită integrarea, iar majoritatea integrărilor se construiesc o dată, funcționează, și nimeni nu le mai revizuiește până nu se strică. Această asimetrie e tot motivul pentru care o schimbare de payload la webhook merită mai multă prudență decât aceeași schimbare într-un body de răspuns pe care un apelant l-a cerut activ.

Ce contează cu adevărat ca schimbare care rupe compatibilitatea într-un payload de webhook?

SchimbareRupe compatibilitatea pentru majoritatea receptorilor
Adăugarea unui câmp nouNu, dacă receptorii ignoră câmpurile necunoscute (verificați această presupunere, nu o luați de bună)
Eliminarea unui câmpDa, dacă ceva îl citește
Redenumirea unui câmpDa, funcțional identic cu eliminarea celui vechi
Schimbarea tipului unui câmp (string în obiect)Da, aproape întotdeauna
Reordonarea câmpurilor în body-ul JSONNu, pentru orice receptor care parsează după cheie, ceea ce ar trebui să fie toți
Schimbarea numelui sau tipului evenimentuluiDa, dacă receptorii filtrează sau rutează pe baza lui

Rândul “adăugarea unui câmp e sigură” e cel pe care echipele se bazează cel mai mult și cel mai merituos să fie verificat, nu presupus. Un parser JSON permisiv ignoră câmpurile necunoscute implicit, dar un receptor care deserializează într-o schemă strictă, câteva limbaje tipizate fac asta fără configurație suplimentară, poate respinge tot payload-ul de îndată ce apare un câmp neașteptat. Adăugarea unui câmp e sigură pentru webhook-ul vostru doar dacă știți cum parsează receptorii, nu pentru că JSON în sine e permisiv.

Cum se versionează un payload de webhook?

La fel ca la un răspuns de API, cu o nuanță: receptorul nu trimite niciodată o cerere, deci nu poate cere o versiune, iar expeditorul trebuie s-o declare. Ea poate merge în body sau într-un header al cererii de livrare însăși; livrările GitHub poartă X-GitHub-Event și X-GitHub-Hook-ID, iar specificația Standard Webhooks își pune metadatele în header-e webhook-*. Un câmp de versiune în payload ("payload_version": 2) e opțiunea cea mai ieftină și funcționează când receptorii sunt dispuși să ramifice pe baza lui. Un tip de eveniment versionat (invoice.updated devine invoice.updated.v2 ca un eveniment distinct la care un receptor se abonează voluntar) cere mai multă muncă de construit dar înseamnă că forma veche continuă să ajungă la cei care nu au migrat niciodată, ceea ce contează mai mult aici decât la un endpoint REST pentru că nu puteți suna fiecare receptor să-i cereți să actualizeze. O setare per abonament, aleasă la înregistrarea endpoint-ului webhook-ului, mută decizia în avans în loc să ramifice la fiecare livrare, și e alegerea corectă când aveți deja o înregistrare de abonament de care s-o atașați.

POST /endpoint-receptor
{
  "event": "invoice.updated",
  "payload_version": 2,
  "data": { "invoice_id": "inv_123", "status": "paid" }
}

Cum știți măcar cine ascultă?

Mai rău decât versiunea echivalentă a acestei probleme într-un changelog de API, pentru că un webhook nu are un jurnal de cereri primite de partea voastră care să numească apelantul; aveți doar propriul jurnal de livrare ieșit, care vă spune că un endpoint a primit un 200, nu ce a făcut cu body-ul. Urmăriți cel puțin două lucruri: fiecare endpoint înregistrat cu o responsabilă, aceeași disciplină pe care changelog-urile de API intern o recomandă pentru consumatorii interni, și rata voastră de eșec al livrării per endpoint după o schimbare de payload. O creștere bruscă a răspunsurilor 4xx sau 5xx de la un endpoint imediat după o schimbare e cel mai apropiat lucru de un stack trace pe care îl veți obține, și adesea singurul semnal că un receptor s-a stricat, pentru că echipa care îl operează poate să nu observe zile întregi.

Ar trebui un changelog de webhook-uri să fie separat de changelog-ul de API?

O secțiune separată pe aceeași pagină, nu o publicație separată. Un changelog de API stabilește deja cine îl citește și cum se abonează cineva; o schimbare de payload la webhook aparține aceluiași flux, etichetată suficient de clar încât o dezvoltatoare de partea receptorului care scanează pentru “asta îmi afectează integrarea” să poată filtra după ea, pentru că o consumatoare de webhook adesea nu are alt motiv să verifice un changelog general de API și îl va găsi doar dacă cineva o îndrumă direct acolo.

Cum ar trebui să arate o fereastră rezonabilă de deprecare pentru un payload de webhook?

Mai lungă decât deprecarea REST echivalentă, pentru că migrarea de partea receptorului înseamnă de obicei că o a doua echipă, cu care poate nu aveți legătură directă, trebuie s-o observe, s-o planifice și s-o lanseze fără urgență proprie. O lună e un minim rezonabil pentru un câmp pe care receptorul îl parsează plauzibil încă cu o bibliotecă permisivă; trei luni sau mai mult sunt mai sigure pentru eliminarea unui câmp pe care o schemă strictă l-ar respinge complet. Trimiteți forma veche și cea nouă împreună în timpul ferestrei când e fezabil (câmpul vechi status și înlocuitorul lui din versiunea 2 în același payload), pentru că un receptor care citește câmpul vechi continuă să funcționeze fără să-și atingă codul, iar unul care a migrat deja pur și simplu ignoră câmpul de care nu mai are nevoie.

FAQ

Consumatorii de webhook-uri trebuie să confirme o schimbare de payload înainte să fie lansată? Nu există un mecanism de confirmare implicit, și tocmai de aceea fereastra de deprecare contează mai mult aici decât la un API REST: nimeni nu confirmă că e pregătit, deci fereastra trebuie să fie suficient de lungă încât majoritatea receptorilor să migreze în propriul ritm înainte ca forma veche să dispară.

E vreodată sigur să adăugați câmpuri necunoscute fără anunț? Doar după ce ați verificat, nu presupus, că receptorii voștri parsează permisiv. O intrare de changelog costă puțin și elimină incertitudinea; adăugarea în tăcere a câmpurilor pe presupunerea că “parserele JSON ignoră extra-urile” strică orice receptor cu deserializare strictă.

Care e cel mai rapid mod de a detecta un receptor de webhook stricat după o schimbare de payload? O rată de eșec al livrării per endpoint, observată în orele imediat următoare schimbării. Nu vă va spune ce s-a stricat, doar că ceva s-a stricat, dar e semnalul cel mai timpuriu și adesea singurul pe care îl veți obține.

Logica de reîncercare ajută receptorii să supraviețuiască unei schimbări de payload? Nu. O reîncercare retrimite același payload nou; nu revine la o formă pe care receptorul o poate parsa. O schimbare de payload strică un receptor la prima livrare și la fiecare reîncercare ulterioară identic.


Afirmațiile tehnice din acest articol nu au fost verificate independent. Dacă ceva nu e corect, spune-ne și vom corecta.

Pe changeloop: Documentație pentru dezvoltatori, Comparație instrumente changelog

changeloop
Echipa care construiește un changelog care închide bucla. Utilizatorii cer ceva, echipa ta livrează, cel care a cerut află.