Zmiany w API

Zmiany łamiące kompatybilność: co się liczy i jak je wydać

8 min czytania zaktualizowano

Zmiana łamiąca kompatybilność to zmiana, której poprawnie napisany wywołujący nie mógłby przetrwać. Definicja ma znaczenie, ponieważ większość sporów o to, czy coś “się liczy”, to tak naprawdę spory o to, kto trzymał to źle. Jeśli wywołujący postępował zgodnie z waszą dokumentacją, a wasza zmiana sprawiła, że jego kod przestał działać, zmiana łamała kompatybilność. To, co zamierzaliście, nie ma z tym nic wspólnego.

To cały test. Reszta tego artykułu to to, co z niego wynika: co go nie przechodzi, co przechodzi, jak wyłapać porażkę, zanim trafi do maina, i co zrobić, gdy już wiecie, że wydajecie jedną z nich.

Co liczy się jako zmiana łamiąca kompatybilność?

Zastosujcie test do wywołującego, nie do diffa. Zmiana łamie kompatybilność, gdy wywołujący, który polegał wyłącznie na udokumentowanym zachowaniu, musi zmienić swój kod, konfigurację lub dane, by nadal działać. Usunięcie pola, zmiana nazwy endpointu, zaostrzenie walidacji, zmiana domyślnej wartości i zmiana typu wartości wszystkie się kwalifikują. Dodanie opcjonalnego pola nie. Naprawienie błędu zwykle nie, z jednym ważnym wyjątkiem poniżej.

ZmianaŁamie kompatybilność?Dlaczego
Usunięcie lub zmiana nazwy pola, endpointu, flagi lub opcjiTakPoprawni wywołujący się do tego odwołują
Dodanie opcjonalnego pola lub nowego endpointuNieIstniejące wywołania się nie zmieniają
Uczynienie opcjonalnego wejścia wymaganymTakWywołania, które je pomijały, teraz zawodzą
Zaostrzenie wcześniej akceptowanej walidacjiTakDane wejściowe, które działały, są teraz odrzucane
Zmiana domyślnej wartościTakWywołujący, którzy jej nie ustawili, dostają nowe zachowanie
Zmiana typu (string na liczbę, pojedyncza wartość na tablicę)TakParsery napisane dla udokumentowanego typu zawodzą
Zmiana kolejności kluczy obiektuNieChyba że udokumentowaliście kolejność
Naprawienie błędu, na którym polegali wywołującyW praktyce takZobacz sekcję o przypadkowych kontraktach
Podniesienie limitu szybkości lub rozmiaruNieNic, co działało, nie przestaje działać
Obniżenie limitu szybkości lub rozmiaruTakRuch, który był w porządku, jest teraz ograniczany
Zmiana sformułowania komunikatu błęduZależyŁamie kompatybilność, jeśli to udokumentowaliście lub wywołujący dopasowują do tego

Co nie jest zmianą łamiącą kompatybilność?

Zmiana nie łamie kompatybilności, gdy każde wywołanie, które działało wcześniej, nadal działa bez zmian i nadal znaczy to samo. Dodanie nowego endpointu, opcjonalnego parametru żądania lub pola w odpowiedzi, uczynienie wymaganego wejścia opcjonalnym, podniesienie limitu i poprawa komunikatu błędu, do którego nikt nic nie dopasowuje, przechodzą test. Takie zmiany addytywne mogą trafić do wydania minor ze zwykłym wpisem w changelogu.

Zmiany addytywne mimo to łamią wywołujących w trzech sytuacjach. Klient, którego deserializer odrzuca nieznane pola, zawodzi na pierwszym nowym polu odpowiedzi, więc dokumentujcie od początku, że wywołujący muszą ignorować pola, których nie rozpoznają. Nowa wartość enum łamie każdego wywołującego z wyczerpującym switch’em (więcej o tym niżej). A rosnąca odpowiedź może wypchnąć wywołującego poza limit rozmiaru, timeout lub szerokość kolumny, o których nigdy nie musiał myśleć.

Cztery wiersze tabeli zasługują na bliższe spojrzenie, ponieważ tam pojawiają się niezgody.

Cztery zmiany łamiące kompatybilność, które pomijają zespoły

Przypadkowe kontrakty. Jeśli wasze API zwracało to samo nieudokumentowane pole przez trzy lata, wywołujący na tym zbudował. Prawo Hyruma to krótka wersja: przy wystarczającej liczbie użytkowników każde obserwowalne zachowanie waszego systemu będzie od kogoś zależne. Dlatego “to była poprawka błędu” nie jest obroną. Poprawka może być poprawna i mimo to łamać kompatybilność. Wydajcie ją jako taką.

Zmiany zachowania bez zmiany schematu. Pole nadal tam jest, typ jest ten sam, a wartość znaczy teraz coś innego. status, który był active lub inactive, a teraz zwraca też suspended, łamie każdego wywołującego z wyczerpującym switch’em. Znacznik czasu przechodzący z czasu lokalnego na UTC łamie każdego, kto nie przeczytał dokumentacji dwa razy. Nic w diffie pliku OpenAPI tego nie pokazuje.

Zaostrzona walidacja. Zaczynacie odrzucać e-maile bez TLD, lub spacje na końcu, lub imiona dłuższe niż 80 znaków. Każdy wywołujący, który wysyłał dokładnie to, teraz dostaje 400 za żądanie, które działało w zeszłym tygodniu. Zmiany walidacji są najczęściej wydawane jako poprawka “utwardzania”.

Zmienione wartości domyślne. Nikt, kto ustawił wartość jawnie, nic nie zauważa. Wszyscy, którzy tego nie zrobili, czyli większość wywołujących, dostają nowe zachowanie bez zmiany ani jednej linii. Zmieniona wartość domyślna łamie większość waszych użytkowników dokładnie dlatego, że nigdy nie widzieli tego ustawienia.

Jak wykryć zmianę łamiącą kompatybilność, zanim zostanie wydana?

Porównajcie kontrakt z pull requesta z kontraktem z głównej gałęzi, w CI, i oblejcie build przy łamiącej różnicy. Narzędzia do diffowania schematów istnieją dla większości formatów interfejsów, a każde zna reguły łamania kompatybilności swojego formatu:

InterfejsNarzędzieCo porównuje
REST (OpenAPI)oasdiffDwie specyfikacje OpenAPI, z raportem zmian łamiących kompatybilność
gRPC (Protobuf)buf breakingPliki .proto, na poziomie wire lub źródła
GraphQLGraphQL InspectorDwa schematy, z oznaczeniem zmian łamiących i niebezpiecznych
Crate’y Rustacargo-semver-checksPubliczne API względem ostatniej opublikowanej wersji
Pakiety TypeScriptAPI ExtractorZatwierdzony raport publicznego API pakietu

Te narzędzia niezawodnie łapią usunięte pola, zmienione nazwy operacji i zmienione typy. Nie widzą pierwszych dwóch z czterech rodzajów powyżej, przypadkowego kontraktu i zmiany zachowania, bo żaden z nich nie pojawia się w schemacie. Użyjcie narzędzia, by zatrzymać oczywiste, a pytania w review “czy poprawny wywołujący mógłby to zauważyć?” dla reszty. To samo zadanie CI to naturalne miejsce, by wymagać wpisu w changelogu, jak opisuje wymuszanie wpisu w changelogu w CI, a zmiany API w gRPC i Protobuf omawiają przypadki na poziomie wire.

Jak oznaczyć zmianę łamiącą kompatybilność w commicie?

W Conventional Commits zmianę łamiącą kompatybilność oznacza ! przed dwukropkiem (feat(api)!: remove the legacy export endpoint) lub stopka zaczynająca się od BREAKING CHANGE: z opisem. Każde z nich odpowiada wersji major. Napiszcie stopkę jako pierwszy szkic wpisu w changelogu, nazywając, kogo to dotyczy i co muszą zrobić. Conventional commits a changelog opisuje, jak daleko sięga ta konwencja.

Ta sama zasada obowiązuje biblioteki. Usunięta funkcja publiczna, zawężony typ parametru lub zmieniona wartość zwracana to wersja major w wersjonowaniu semantycznym. Biblioteki nie zawsze się tego trzymają: badanie 119 879 aktualizacji z Maven Central wykazało, że 16,6% naruszyło wersjonowanie semantyczne, a mimo to dotknęło to tylko 7,9% projektów klienckich, bo większość tych zmian dotyczyła kodu, którego żaden klient nie wywoływał. Złamanie kompatybilności mierzy się u wywołującego.

Jak wydaje się zmianę łamiącą kompatybilność?

Wydajecie ją otwarcie, z datą, ze ścieżką. Kroki poniżej są w kolejności, a ostatni to ten, który pomija większość zespołów: powiedzenie ludziom, których to dotknęło, że to, na co czekali, już się wydarzyło.

  1. Zdecydujcie, czy to ona. Użyjcie testu powyżej, nie diffa. Jeśli dwoje inżynierów się nie zgadza, to łamie kompatybilność; niezgoda jest dowodem, że wywołujący mógł rozsądnie polegać na starym zachowaniu.
  2. Wersjonujcie ją. Pod wersjonowaniem semantycznym zmiana łamiąca kompatybilność to wersja major. Jeśli prowadzicie API datowane lub wersjonowane, trafia do nowej wersji, a stara nadal działa do zadeklarowanej daty. Jeśli nie możecie wersjonować, nie wydajecie zmiany łamiącej kompatybilność, wydajecie awarię z wpisem changelogu. To, który schemat niesie wersję, jest tematem najlepszych praktyk wersjonowania API.
  3. Napiszcie wpis przed zmergowaniem kodu. Wpis ma stałą formę: co się zmienia, kogo dotyczy, co muszą zrobić, i do kiedy. Jeśli nie możecie wypełnić wszystkich czterech, zmiana nie jest gotowa. Szablon release notes stawia te wpisy jako pierwsze, z datą zamiast numeru wersji, dokładnie z tego powodu.
  4. Podajcie termin, nie numer wydania. “Usunięte w v5” nic nie znaczy dla kogoś, kto nie śledzi waszych wydań. “Przestaje działać 1 listopada 2026” znaczy to samo dla wszystkich.
  5. Zapewnijcie migrację. Przykład kodu ze starego wywołania obok nowego. Jeśli zmiana to zmiana nazwy, podajcie obie nazwy w tym samym zdaniu. Jeśli to usunięte pole, powiedzcie, dokąd trafiły dane.
  6. Ogłoście wszędzie, gdzie stare zachowanie było udokumentowane. Changelog, stronę dokumentacji opisującą endpoint, release notes SDK, i nagłówek deprecacji w odpowiedzi, jeśli go macie. Ogłoszone w jednym miejscu jest ogłoszone ludziom, którzy akurat tam spojrzeli.
  7. Zamknijcie pętlę. Jeśli klientka poprosiła o zmianę, lub zgłosiła błąd, który do niej doprowadził, powiedzcie jej, gdy zostanie wydana. To krok, który zmienia to z czegoś zrobionego waszym użytkownikom w coś zrobionego razem z nimi.

Jak wygląda dobry wpis o zmianie łamiącej kompatybilność?

Dobry wpis nazywa dotkniętego wywołującego w pierwszej linii, podaje datę, i zawiera poprawkę. Oto jeden dla przypadku zaostrzonej walidacji, w formie, której używamy:

Adresy e-mail bez domeny są odrzucane od 1 listopada 2026. POST /users i PATCH /users/:id obecnie akceptują wartości email takie jak alice@localhost. Od 1 listopada te zwracają 400 invalid_email. Dotyczy każdej integracji, która tworzy użytkowników z wewnętrznych katalogów. Migracja: wyślijcie w pełni kwalifikowany adres, lub pomińcie pole i ustawcie je później. Żadna zmiana nie jest potrzebna, jeśli wasze adresy już mają domenę, co dotyczy 99,4% kont utworzonych w tym roku.

Gdzie to powiadomienie powinno mieszkać, i co jeszcze powinno mu towarzyszyć, omawia changelog API.

Procent na końcu to nie dekoracja. Mówi czytelniczce, czy powinna się martwić, co jest pytaniem, z którym otworzyła wpis.

Dlaczego po prostu ich nie unikać?

Ponieważ alternatywa jest gorsza. API, które nigdy niczego nie psuje, gromadzi każdy błąd, jaki kiedykolwiek popełniło: źle nazwane pole, złą wartość domyślną, znacznik czasu w czasie lokalnym. Każdy z nich to podatek dla każdego nowego wywołującego na zawsze, by chronić wywołujących, którzy mogliby migrować w jedno popołudnie. Zespoły z najlepszą reputacją stabilności rzadko coś psują, według harmonogramu, ze ścieżką migracji i ostrzeżeniem, które dotarło do ludzi, dla których było przeznaczone.

Mechanika tego ostrzeżenia jest tematem towarzyszącego artykułu o deprecjonowaniu API. Wpis, który to ogłasza, jest przygotowywany w ten sam sposób co każdy inny wpis w kanale changelogu: ze zmergowanego pull requesta, zatrzymany dla człowieka, a potem opublikowany w miejscu, gdzie dotknięci wywołujący już czytają.

FAQ

Jaka jest różnica między zmianą łamiącą a niełamiącą kompatybilność? Zmiana łamiąca zmusza poprawnego wywołującego do zmiany kodu, konfiguracji lub danych, by nadal działał. Zmiana niełamiąca zostawia każde istniejące wywołanie działającym z tym samym znaczeniem, dlatego dodania zwykle są bezpieczne, a usunięcia, zmiany nazw i zaostrzone reguły zwykle nie.

Czy dodanie wymaganego pola się liczy? Tak. Każde istniejące wywołanie je pomija, więc każde istniejące wywołanie teraz zawodzi. Dodajcie je jako opcjonalne z sensowną wartością domyślną, lub wersjonujcie endpoint.

Czy poprawka błędu się liczy? Może. Jeśli wywołujący polegali na wadliwym zachowaniu, naprawienie go ich łamie, bez względu na to, co mówiła dokumentacja. Traktujcie każdą poprawkę zmieniającą obserwowalny wynik jako łamiącą kompatybilność, chyba że możecie wykazać, że nikt na niej nie polegał.

Czy wersjonowanie semantyczne dotyczy API webowego? Zasada tak: zmiany łamiące kompatybilność dostają nową wersję major, a stara nadal działa przez zadeklarowany okres. Numer często żyje w URL lub nagłówku daty zamiast w wersji pakietu.

Ile wyprzedzenia wystarczy? Wystarczająco, by wywołujący znalazł powiadomienie i wykonał pracę. Dziewięćdziesiąt dni to powszechna dolna granica dla publicznych API; dłużej dla wszystkiego, co jest używane w kodzie dostarczanym użytkownikom końcowym i nie może być zaktualizowane zdalnie.


Twierdzenia techniczne w tym artykule nie zostały niezależnie zweryfikowane. Jeśli coś się nie zgadza, daj nam znać, a poprawimy to.

Powiązane w changeloop: Szablon notatek wydania, Dokumentacja dla deweloperów

changeloop
Zespół, który tworzy changelog zamykający pętlę. Użytkownicy o coś proszą, Twój zespół to dostarcza, proszący się dowiaduje.