Breaking API Changes erklärt

Ein Breaking API Change ist jede Änderung am veröffentlichten Vertrag einer API, die einen Client, der korrekt gegen den alten Vertrag geschrieben wurde, gegen den neuen scheitern lassen kann — ohne dass der Client irgendetwas ändert. Dieser eine Satz ist die ganze Theorie; alles andere in diesem Artikel sind seine Folgen: welche Änderungen zählen, welche berühmten Fallen harmlos aussehen und es nicht sind, wie man ein Spezifikations-Upgrade vor dem Deployment prüft und wie Versionierungsstrategien Clients Zeit kaufen. Um es auf zwei echte Dokumente anzuwenden, führen Sie den OpenAPI Diff über die alte und die neue Spezifikation aus und lesen Sie die Urteile neben dieser Seite.

Ein Vertrag ist ein Versprechen über die Zukunft

Eine API-Spezifikation ist keine Dokumentation im beiläufigen Sinn; sie ist ein Versprechen. Wenn eine Spezifikation deklariert, dass GET /pets einen optionalen Parameter limit akzeptiert und mit 200 und einem Objekt mit einem String name antwortet, baut jeder Client-Autor genau darauf: Der Parameter wird weggelassen, wenn er nicht gebraucht wird, der Status wird verzweigt, das Feld wird als String gelesen. Die Korrektheit des Clients wird am Versprechen gemessen, weshalb die Definition von Breaking asymmetrisch ist — eine Änderung ist Breaking, wenn ein korrekter alter Client scheitern kann, selbst wenn die meisten Clients zufällig überleben. Ein striktes, aus der alten Spezifikation generiertes SDK, das an einem geänderten Typ scheitert, genügt: Das Versprechen wurde gebrochen, nicht der Client.

Dieser Rahmen löst die meisten Argumente darüber, ob eine Änderung «zählt». Einen Endpunkt zu entfernen ist Breaking, weil das Versprechen seine Existenz einschloss. Einen Endpunkt hinzuzufügen ist es nicht, weil kein alter Client sich selbst irgendetwas über Pfade versprochen hat, die er nie aufruft. Ein neues optionales Antwortfeld ist sicher, weil alte Parser unbekannte Felder ignorieren — in jedem vernünftigen Format kann additive Ausgabe einen Leser nicht ungültig machen. Eine neue Pflicht-Eingabe dagegen reicht in jede bestehende Anfrage hinein und macht die ungültig, die sie auslassen. Der Test ist immer derselbe: Spielen Sie den gültigen Verkehr von gestern gegen die Regeln von morgen und fragen Sie, was jetzt scheitert.

Zwei Folgerungen zählen in der Praxis. Erstens ist der Vertrag das, was veröffentlicht ist, nicht das, was der Server heute zufällig tut: Ein undokumentiertes Feld, das Clients durch Verkehrsschnüffeln entdeckt haben, ist nicht versprochen, und es zu entfernen ist unhöflich, aber streng genommen nicht Breaking — was ein Argument dafür ist, genau das zu dokumentieren, was Sie zu behalten beabsichtigen, und nicht mehr. Zweitens umfasst der Vertrag die ganze Oberfläche der Standards, auf denen er steht: Die von RFC 9110 fixierte HTTP-Methoden- und Statussemantik ist Teil des Versprechens, sodass die Neudefinition dessen, was ein 200 oder ein 404 bedeutet, eine Vertragsänderung ist, selbst wenn die Spezifikationsdatei Statusbedeutungen nie erwähnt.

Die klassischen Fallen

Der häufigste Produktionsbruch ist die Hinzufügung eines Pflichtfelds. Ein Team fügt einem Anfragekörper ein Feld hinzu, markiert es als Pflicht, weil die neue Geschäftsregel es braucht, und liefert aus — woraufhin jeder bestehende Client, von denen keiner das Feld sendet, anfängt, 400er zu erhalten. Die sicheren Versionen sind langweilig: Fügen Sie das Feld als optional mit einem serverseitigen Standard hinzu, oder verlangen Sie es nur an einem neuen Endpunkt. Die zweite Falle ist die Enum-Entfernung: Ein Statuswert, eine Kategorie, eine Sortierung wird stillgelegt, weil die neue Oberfläche sie nicht mehr anbietet, während gespeicherte Clientdaten, mobile Apps auf alten Builds und Partnerintegrationen sie noch jahrelang senden. Enum-Werte kosten fast nichts, weiter akzeptiert zu werden; einen zurückzuziehen sollte behandelt werden wie das Löschen eines Endpunkts.

Typänderungen sind die dritte Falle, und sie kommen in Verkleidungen der Verengung und der Erweiterung. Verengung — String zu Enum, Integer zu einem kleineren Bereich, freies Objekt zu fester Form — lehnt Eingaben ab, die früher gültig waren, und ist auf den ersten Blick Breaking. Erweiterung ist subtiler: Ein Antwortfeld von String zu String-oder-Null oder eine Integer-Id zu einer String-Id zu ändern sieht großzügig aus und bricht jeden strikten Parser und jedes generierte SDK, das für den alten Typ gebaut wurde. Auf der Antwortseite gibt es eine Spiegelfalle: Ein Feld, das immer vorhanden war, wird optional. Clients lesen es bedingungslos; die erste Antwort, die es auslässt, ist ein Null-Pointer in der Codebasis eines anderen.

Die vierte Falle ist semantisch, und kein Differ kann sie fangen: Das Feld behält Name und Typ und ändert seine Bedeutung. Ein Preis, der still von Cent zu Dollar wechselt, ein Zeitstempel, der von UTC zu lokaler Zeit wandert, eine Liste, die sortiert war und es nicht mehr ist — der Spezifikations-Diff ist leer und jeder Client liegt falsch. Semantische Brüche sind der Grund, warum der mechanische Diff ein Boden und keine Decke für die Prüfung ist: Er räumt die Änderungen weg, die niemanden brechen können, damit sich die menschliche Aufmerksamkeit auf die Änderungen konzentrieren kann, die es bloß könnten.

Ein Anbieter-Spec-Upgrade vor dem Deployment prüfen

Anbieter veröffentlichen ständig neue Spezifikationsversionen, und «SDK anheben und neu deployen» ist der Weg, auf dem stille Brüche die Produktion erreichen. Eine disziplinierte Prüfung dauert fünfzehn Minuten. Erstens beschaffen Sie beide Dokumente — die Spezifikation, gegen die Ihr Deployment heute tatsächlich läuft, und die Kandidatin — und diffen Sie sie mechanisch, statt ein Changelog zu lesen, denn Changelogs sind Marketingdokumente: Sie listen Funktionen, nicht Entfernungen. Der OpenAPI Diff erzeugt diese Liste lokal, was zählt, wenn die API intern ist und ihre Spezifikation nicht in das gehostete Tool eines Fremden eingefügt werden sollte.

Zweitens arbeiten Sie die Breaking-Liste als Checkliste gegen Ihre eigene Nutzung ab. Die meisten Anbieterbrüche sind für Sie theoretisch: ein entfernter Endpunkt, den Sie nie aufrufen, ein neues Pflichtfeld an einer Ressource, die Sie nie erstellen. Die gefährlichen Einträge sind die, die sich mit Ihrem Verkehr schneiden — weshalb die Prüfung Ihr Aufruf-Inventar braucht, nicht nur den Diff. Drittens prüfen Sie die undokumentierte Oberfläche: Status, an denen Sie verzweigen, Fehlerformen, die Sie parsen, Header, die Sie lesen. Dieselbe Gewohnheit, die Webhook-Konsumenten schützt — den Nutzlastvertrag festzunageln und zu testen, wie in Webhook-Signaturen erklärt — gilt für jeden Vertrag, den ein anderer kontrolliert.

Schließlich führen Sie es stufenweise ein. Zeigen Sie mit einem Staging-Client auf die neue Version, spielen Sie aufgezeichnete Produktionsanfragen erneut ab (eine bereinigte Aufzeichnung, behandelt wie es der HAR-Leitfaden vorschreibt) und vergleichen Sie die Antworten, bevor es irgendein Nutzer tut. Wenn der Anbieter ein Deprecation-Fenster oder einen Versions-Pin anbietet, nehmen Sie ihn: Der billigste Breaking Change ist der, den Sie in Ihrem eigenen Tempo übernehmen, nachdem Ihre Clients bereit sind, statt am Veröffentlichungstag des Anbieters.

Versionierungsstrategien: URL, Header oder nie brechen

Die klassische Antwort auf Breaking Change ist ein Versionsbezeichner, und das klassische Argument ist, wohin man ihn setzt. URL-Versionierung (/v1/, /v2/) ist explizit, cache-freundlich, trivial routbar und in jeder Logzeile sichtbar; ihr Preis ist, dass sie die ganze API auf einmal versioniert, sodass ein einziger brechender Endpunkt jeden Client durch eine Migration zieht. Header-Versionierung — ein Versions-Header oder ein datiertes Content-Negotiation-Schema — hält URLs sauber und kann einzelne Ressourcen versionieren, zum Preis der Unsichtbarkeit: Die Version verschwindet aus Logs, Links und Browserleisten, und Debugging wird Archäologie. Keines von beiden ist falsch; die Industrie zahlt nur die URL-Kosten bereitwilliger, weil Operabilität um drei Uhr morgens Eleganz schlägt.

Die tiefere Strategie ist, Versionen selten zu brauchen. Nur additive Evolution — nie entfernen, nie verlangen, nie Typen ändern; nur optionale Dinge hinzufügen — hält eine einzige Version unbegrenzt am Leben, und genau so operieren die langlebigsten öffentlichen APIs tatsächlich. Sie verlangt Disziplin im Voraus: großzügige Eingabetoleranz, Ids, die vom ersten Tag an als Strings typisiert sind (damit der Bereich wachsen kann), Enums, die zum Erweitern entworfen sind, und ein Deprecation-Prozess, der lange warnt, bevor irgendetwas zurückgezogen wird. Semantische Versionierung, wie sie bei semver.org kodifiziert ist, liefert die Signalschicht, die die eigene Spezifikation der OpenAPI Initiative bewusst den Publishern überlässt: Eine Major-Anhebung kündigt einen Bruch an, und Clients können sich dagegen pinnen.

Welche Strategie auch gewählt wird, sie funktioniert nur als sozialer Vertrag, der von Mechanik gestützt ist: Versionen müssen für ein erklärtes Fenster wirklich nebeneinander laufen, Deprecations müssen dort angekündigt werden, wo Client-Autoren sie sehen (Changelog, Antwort-Header, direkte Mitteilung für die großen Konsumenten), und die Nutzung der alten Version muss gemessen werden, damit «niemand benutzt sie mehr» eine Tatsache ist, keine Hoffnung. Eine Version, die veröffentlicht wird, aber nicht parallel betrieben werden kann, ist keine Strategie; sie ist eine Umbenennung des Bruchs.

Wo der Differ hineinpasst

Ein mechanischer Differ verdient seinen Platz zu zwei Zeitpunkten. In der Prüfung verwandelt er «Ist irgendetwas gebrochen?» von einem Nachmittag nebeneinander Lesens in eine Liste, die abgehakt werden kann — und, ebenso nützlich, er beweist ein Negativ: Wenn ein Release behauptet, nur additiv zu sein, ist eine leere Breaking-Liste ein Nachweis, kein Gefühl. In der Continuous Integration kann derselbe Vergleich bei jedem Pull Request laufen, der eine Spezifikation berührt, und den Build scheitern lassen, wenn ein Breaking Change ohne die Versionsanhebung oder Genehmigung ankommt, die die Projekt-Politik verlangt. Die Klassifikationstabelle ist die Politik; das Tool weigert sich nur, sie zur Deadline vergessen zu lassen.

Seine Grenzen definieren den Rest des Workflows. Der Differ liest nur den deklarierten Vertrag: Er kann Verhalten, Leistung, Semantik oder die undokumentierten Eigenheiten, von denen Clients abhängen, nicht sehen, und er fasst pathologisch tiefe Schemata zusammen, statt sie für immer zu expandieren. Das vollständige Tor ist also eine Pipeline, keine Seite: mechanischer Diff für den Vertrag, Vertragstests, die echte Client-Aufrufe wieder abspielen, für das Verhalten, stufenweise Einführung für die Semantik und Monitoring für den Tag, an dem alle drei etwas übersehen. So gebraucht ist der Diff die billigste der vier Schichten — fünfzehn Sekunden, die die peinlichste Klasse von Ausfall löschen, die, bei der der Bruch die ganze Zeit in der Spezifikation stand und niemand die Dokumente verglichen hat.

Das ist die Gewohnheit, für die dieser Artikel wirklich argumentiert. Spezifikationen sind billig zu behalten und billiger zu vergleichen; Vorfälle sind es nicht. Behalten Sie jede veröffentlichte Version jedes Vertrags, den Sie besitzen oder konsumieren, diffen Sie jeden Kandidaten gegen die Version in Produktion, bevor er ausgeliefert wird, und behandeln Sie die Breaking-Liste als die wirkliche Tagesordnung des Release. Die Teams, die das tun, hören auf, darüber zu streiten, ob eine Änderung jemanden «hätte brechen sollen» — sie haben die Liste, datiert und archiviert, von vor der Auslieferung.

Quellen und weiterführende Lektüre

Häufig gestellte Fragen

Ist das Hinzufügen eines Pflichtfelds immer Breaking?

Ja, an jedem Endpunkt, den bestehende Clients bereits aufrufen: Anfragen, die gestern gültig waren, werden heute abgelehnt, und das ist die Definition von Breaking. Die Standardauswege sind, das Feld als optional mit einem serverseitigen Standard hinzuzufügen, es nur an einem neuen Endpunkt oder einer neuen Version zu verlangen, oder ein Deprecation-Fenster anzukündigen, in dem das Feld akzeptiert, aber gewarnt wird, bevor es Pflicht wird.

URL-Versionierung oder Header-Versionierung — was ist besser?

Für die meisten Teams die URL-Versionierung: Sie ist in Logs und Links sichtbar, leicht zu routen und zu cachen und ehrlich darüber, was sie ist. Die Header-Versionierung passt zu Plattformen, die Ressourcen-Granularität brauchen und bereits eine starke Observability haben. Beide schlagen die schlechteste Option, den unversionierten Breaking Change. Die beste Strategie überhaupt ist die nur additive Evolution, die Versionierung zu einem seltenen Ereignis statt zu einer Routine-Steuer macht.

Kann ein Tool jeden Breaking Change fangen?

Nein. Ein Differ fängt alles, was die Spezifikation deklariert: Entfernungen, neue Pflicht-Eingaben, Typänderungen, Enum-Rückzüge. Er kann semantische Änderungen — gleiches Feld, neue Bedeutung — oder Verhalten, das die Spezifikation nie deklariert hat, nicht fangen. Deshalb gehört der mechanische Diff in eine Pipeline mit Vertragstests und stufenweiser Einführung, nicht auf einen eigenen Sockel.