Modificări de API

Schimbări care rup compatibilitatea și cum le lansezi

10 min de citit actualizat pe

O schimbare care rupe compatibilitatea e o schimbare pe care un apelant scris corect n-ar fi putut-o supraviețui. Definiția contează pentru că majoritatea disputelor despre dacă ceva “contează” sunt de fapt dispute despre cine a ținut-o greșit. Dacă un apelant a urmat documentația voastră și schimbarea voastră a făcut codul lui să nu mai funcționeze, schimbarea rupea compatibilitatea. Ce ați intenționat n-are nicio legătură cu asta.

Ăsta e tot testul. Restul acestui articol e ce rezultă din el: ce nu trece testul, ce trece, cum prinzi un eșec înainte de integrare și ce trebuie făcut odată ce știți că lansați una.

Ce contează ca o schimbare care rupe compatibilitatea?

Aplicați testul apelantului, nu diff-ului. O schimbare rupe compatibilitatea când un apelant care s-a bazat doar pe comportamentul documentat trebuie să-și schimbe codul, configurația sau datele ca să continue să funcționeze. Eliminarea unui câmp, redenumirea unui endpoint, strângerea validării, schimbarea unei valori implicite și schimbarea tipului unei valori se califică toate. Adăugarea unui câmp opțional nu. Corectarea unei erori de obicei nu, cu o excepție importantă mai jos.

SchimbareRupe compatibilitatea?De ce
Eliminarea sau redenumirea unui câmp, endpoint, flag sau opțiuneDaApelanții corecți fac referire la asta
Adăugarea unui câmp opțional sau a unui endpoint nouNuApelurile existente nu se schimbă
Transformarea unei intrări opționale în obligatorieDaApelurile care o omiteau eșuează acum
Strângerea unei validări acceptate anteriorDaIntrări care funcționau sunt acum respinse
Schimbarea unei valori impliciteDaApelanții care n-au setat-o primesc comportament nou
Schimbarea unui tip (string la număr, valoare unică la array)DaParserele scrise pentru tipul documentat eșuează
Reordonarea cheilor unui obiectNuDoar dacă ați documentat ordinea
Corectarea unei erori pe care se bazau apelanțiiÎn practică daVedeți secțiunea despre contracte accidentale
Ridicarea unei limite de rată sau de dimensiuneNuNimic ce funcționa nu se oprește
Coborârea unei limite de rată sau de dimensiuneDaTrafic care era în regulă e acum limitat
Schimbarea formulării unui mesaj de eroareDepindeRupe compatibilitatea dacă ați documentat-o sau apelanții se potrivesc pe ea

Ce nu e o schimbare care rupe compatibilitatea?

O schimbare nu rupe compatibilitatea când fiecare apel care funcționa înainte funcționează în continuare, neschimbat, și înseamnă același lucru. Adăugarea unui endpoint nou, a unui parametru opțional de cerere, a unui câmp într-un răspuns, transformarea unei intrări obligatorii în opțională, ridicarea unei limite și îmbunătățirea unui mesaj de eroare pe care nu se potrivește nimeni trec toate testul. Aceste schimbări aditive pot fi lansate într-o versiune minor, cu o intrare obișnuită de changelog.

Schimbările aditive tot pot rupe apelanți în trei situații. Un client al cărui deserializator respinge câmpurile necunoscute eșuează la primul câmp nou din răspuns, deci documentați din timp că apelanții trebuie să ignore câmpurile pe care nu le recunosc. O valoare nouă de enum rupe orice apelant cu un switch exhaustiv (mai multe mai jos). Iar un răspuns care crește poate împinge un apelant peste o limită de dimensiune, un timeout sau o lățime de coloană la care nu a trebuit niciodată să se gândească.

Patru rânduri din tabel merită o privire mai atentă, pentru că acolo apar dezacordurile.

Cele patru schimbări care rup compatibilitatea pe care echipele le omit

Contracte accidentale. Dacă API-ul vostru a returnat același câmp nedocumentat timp de trei ani, un apelant a construit pe el. Legea lui Hyrum e versiunea scurtă: cu suficienți utilizatori, fiecare comportament observabil al sistemului vostru va fi dependent pentru cineva. De aceea “a fost o corecție de eroare” nu e o apărare. Corecția poate fi corectă și totuși să rupă compatibilitatea. Lansați-o ca atare.

Schimbări de comportament fără schimbare de schemă. Câmpul e încă acolo, tipul e același, iar valoarea înseamnă acum altceva. Un status care era active sau inactive și acum returnează și suspended rupe fiecare apelant cu un switch exhaustiv. Un timestamp care trece de la ora locală la UTC rupe pe oricine n-a citit documentația de două ori. Nimic într-un diff al fișierului OpenAPI nu arată asta.

Validare strânsă. Începeți să respingeți e-mailuri fără TLD, sau spații la final, sau nume mai lungi de 80 de caractere. Fiecare apelant care trimitea exact asta primește acum un 400 pentru o cerere care funcționa săptămâna trecută. Schimbările de validare sunt cel mai des lansate ca o corecție de “întărire”.

Valori implicite schimbate. Nimeni care a setat valoarea explicit nu observă nimic. Toți cei care n-au făcut asta, care sunt majoritatea apelanților, primesc comportament nou fără să schimbe o linie. O valoare implicită schimbată rupe majoritatea utilizatorilor voștri exact pentru că nu au văzut niciodată setarea.

Cum detectezi o schimbare care rupe compatibilitatea înainte de lansare?

Comparați contractul din pull request cu cel din ramura principală, în CI, și opriți build-ul la o diferență care rupe compatibilitatea. Există unelte de comparare a schemelor pentru majoritatea formatelor de interfață, iar fiecare cunoaște regulile de ruptură ale formatului său:

InterfațăUnealtăCe compară
REST (OpenAPI)oasdiffDouă specificații OpenAPI, cu un raport al schimbărilor care rup compatibilitatea
gRPC (Protobuf)buf breakingFișiere .proto, la nivel de wire sau de sursă
GraphQLGraphQL InspectorDouă scheme, cu semnalarea schimbărilor care rup compatibilitatea și a celor periculoase
Crate-uri Rustcargo-semver-checksAPI-ul public față de ultima versiune publicată
Pachete TypeScriptAPI ExtractorUn raport versionat în repo al API-ului public al pachetului

Aceste unelte găsesc sigur câmpurile eliminate, operațiile redenumite și tipurile schimbate. Nu pot vedea primele două dintre cele patru tipuri de mai sus, un contract accidental sau o schimbare de comportament, pentru că niciuna nu apare într-o schemă. Folosiți unealta ca să opriți cazurile evidente și întrebarea de review „ar putea un apelant corect să observe asta?” pentru restul. Același job de CI e un loc firesc pentru a cere o intrare de changelog, după cum e descris în aplicarea intrărilor de changelog în CI, iar schimbările de API gRPC și Protobuf parcurge cazurile de la nivel de wire.

Cum marchezi o schimbare care rupe compatibilitatea într-un commit?

Cu Conventional Commits, o schimbare care rupe compatibilitatea se marchează cu un ! înainte de două puncte (feat(api)!: remove the legacy export endpoint) sau cu un footer care începe cu BREAKING CHANGE: urmat de o descriere. Oricare dintre ele corespunde unei versiuni major. Scrieți footer-ul ca prima ciornă a intrării de changelog, numind cine e afectat și ce trebuie să facă. Conventional commits și changelog-ul arată până unde duce convenția.

Aceeași regulă e valabilă pentru biblioteci. O funcție publică eliminată, un tip de parametru îngustat sau o valoare returnată schimbată înseamnă o versiune major sub versionarea semantică. Bibliotecile nu o urmează întotdeauna: un studiu pe 119.879 de upgrade-uri din Maven Central a găsit că 16,6% au încălcat versionarea semantică, dar doar 7,9% dintre proiectele client au fost afectate, pentru că majoritatea schimbărilor atingeau cod pe care niciun client nu-l apela. Ruptura se măsoară la apelant.

Cum se lansează o schimbare care rupe compatibilitatea?

O lansați deschis, cu o dată, cu o cale. Pașii de mai jos sunt în ordine, iar ultimul e cel pe care-l omit majoritatea echipelor: să le spuneți oamenilor afectați că lucrul pe care-l așteptau s-a întâmplat acum.

  1. Decideți dacă e una. Folosiți testul de mai sus, nu diff-ul. Dacă doi ingineri nu sunt de acord, rupe compatibilitatea; dezacordul e dovada că un apelant s-ar fi putut baza rezonabil pe comportamentul vechi.
  2. Versionați-o. Sub versionarea semantică o schimbare care rupe compatibilitatea e o versiune major. Dacă operați un API datat sau versionat, merge într-o versiune nouă, iar cea veche continuă să funcționeze până la o dată declarată. Dacă nu puteți versiona, nu lansați o schimbare care rupe compatibilitatea, lansați o întrerupere cu o intrare de changelog. Ce schemă poartă versiunea e subiectul celor mai bune practici de versionare a API-ului.
  3. Scrieți intrarea înainte ca codul să fie integrat. Intrarea are o formă fixă: ce se schimbă, pe cine afectează, ce trebuie să facă, și până când. Dacă nu puteți completa toate cele patru, schimbarea nu e gata. Șablonul de release notes pune aceste intrări primele, cu o dată în loc de un număr de versiune, exact pentru asta.
  4. Dați un termen limită, nu un număr de lansare. “Eliminat în v5” nu înseamnă nimic pentru cineva care nu urmărește lansările voastre. “Nu mai funcționează pe 1 noiembrie 2026” înseamnă același lucru pentru toată lumea.
  5. Furnizați migrarea. Un exemplu de cod al vechiului apel lângă cel nou. Dacă schimbarea e o redenumire, spuneți ambele nume în aceeași propoziție. Dacă e un câmp eliminat, spuneți unde au mers datele.
  6. Anunțați peste tot unde comportamentul vechi era documentat. Changelog-ul, pagina de documentație care descrie endpoint-ul, release notes ale SDK-ului, și header-ul de depreciere în răspuns dacă aveți unul. Anunțat într-un singur loc e anunțat oamenilor care s-au uitat întâmplător acolo.
  7. Închideți bucla. Dacă o clientă a cerut schimbarea, sau a raportat eroarea care a dus la ea, spuneți-i când e lansată. Ăsta e pasul care transformă asta din ceva făcut utilizatorilor voștri în ceva făcut împreună cu ei.

Cum arată o intrare bună despre o schimbare care rupe compatibilitatea?

O intrare bună numește apelantul afectat pe prima linie, declară data, și include corecția. Iată una pentru cazul validării strânse, în forma pe care o folosim:

Adresele de e-mail fără domeniu sunt respinse începând cu 1 noiembrie 2026. POST /users și PATCH /users/:id acceptă în prezent valori email precum alice@localhost. Începând cu 1 noiembrie, acestea returnează 400 invalid_email. Afectează orice integrare care creează utilizatori din directoare interne. Migrare: trimiteți o adresă complet calificată, sau omiteți câmpul și setați-l mai târziu. Nu e necesară nicio schimbare dacă adresele voastre au deja un domeniu, ceea ce e adevărat pentru 99,4% din conturile create anul acesta.

Unde ar trebui să locuiască această notificare, și ce altceva ar trebui să o însoțească, e subiectul changelog-ului de API.

Procentul de la final nu e decorație. Îi spune cititoarei dacă ar trebui să-și facă griji, ceea ce e întrebarea cu care a deschis intrarea.

De ce să nu le evitați pur și simplu?

Pentru că alternativa e mai rea. Un API care nu rupe niciodată nimic acumulează fiecare greșeală pe care a făcut-o vreodată: câmpul denumit greșit, valoarea implicită greșită, timestamp-ul în ora locală. Fiecare e o taxă pentru fiecare apelant nou pentru totdeauna, ca să protejeze apelanții care ar fi putut migra într-o după-amiază. Echipele cu cea mai bună reputație pentru stabilitate rup lucruri rareori, după un program, cu o cale de migrare și un avertisment care a ajuns la oamenii pentru care era destinat.

Mecanica acelui avertisment e subiectul articolului însoțitor despre deprecierea unui API. Intrarea care o anunță e redactată în același mod ca orice altă intrare din fluxul de changelog: din pull request-ul integrat, reținută pentru un om, apoi publicată în locul unde apelanții afectați deja citesc.

FAQ

Care e diferența dintre o schimbare care rupe compatibilitatea și una care n-o rupe? O schimbare care rupe compatibilitatea obligă un apelant corect să-și schimbe codul, configurația sau datele ca să continue să funcționeze. Una care n-o rupe lasă fiecare apel existent funcțional, cu același sens, de aceea adăugările sunt de obicei sigure, iar eliminările, redenumirile și regulile strânse de obicei nu.

Contează adăugarea unui câmp obligatoriu? Da. Fiecare apel existent îl omite, deci fiecare apel existent eșuează acum. Adăugați-l ca opțional cu o valoare implicită sensibilă, sau versionați endpoint-ul.

Contează o corecție de eroare? Poate fi. Dacă apelanții se bazau pe comportamentul cu eroare, corectarea lui îi rupe, indiferent ce spunea documentația. Tratați orice corecție care schimbă rezultatul observabil ca rupând compatibilitatea, decât dacă puteți arăta că nimeni nu se baza pe ea.

Se aplică versionarea semantică la un API web? Regula da: schimbările care rup compatibilitatea primesc o nouă versiune major, iar cea veche continuă să funcționeze pentru o perioadă declarată. Numărul trăiește adesea în URL sau un header de dată în loc de o versiune de pachet.

Cât preaviz e suficient? Suficient ca un apelant să găsească preavizul și să facă munca. Nouăzeci de zile e un prag comun pentru API-uri publice; mai lung pentru orice folosit în cod livrat utilizatorilor finali și care nu poate fi actualizat de la distanță.


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

Pe changeloop: Șablon note de lansare, Documentație pentru dezvoltatori

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