Модуль 5 · Версии и изменения

Справочник «Стандартизация HTTP API» · модуль 5 · Версии и изменения

Глава 5.2

5.2

Breaking
changes

Несовместимое изменение — это изменение, после которого работающий клиент перестаёт работать. Определение простое, но одно и то же изменение GitHub относит к безопасным, а Azure — к ломающим. В этой главе разобрано, откуда берётся это расхождение, как записывают собственную политику совместимости и как её проверяет инструмент сравнения описаний.

Категория
Версии
Уровень
Средний
Связанные главы
5.1 Четыре номера · 6.4 CI и поиск breaking changes · 5.3 Deprecation и Sunset
Зачем это знать
Чтобы не сломать чужой код изменением, которое казалось безобидным

§1Что значит «сломать клиента»

Определение звучит одинаково у всех, и оно намеренно широкое.

GitHub Docs, API Versionsправила компании

«Breaking changes are changes that can potentially break an integration.»

Перевод. Breaking changes — это изменения, которые потенциально могут сломать интеграцию.

Ключевое слово — potentially. Речь не о том, сломалось ли что-то у конкретного клиента, а о том, могло ли сломаться. Поэтому бесспорных случаев немного, и начинать стоит с них.

Ломает почти наверняка
  • удалить операцию
  • переименовать поле ответа
  • изменить тип поля
  • сделать необязательный параметр обязательным
  • убрать значение из перечисления
  • ужесточить проверку тела запроса
Не ломает почти наверняка
  • добавить новую операцию
  • добавить необязательный параметр
  • добавить новый заголовок ответа
  • исправить опечатку в описании
  • добавить новый код ответа в новой ситуации

Между этими двумя списками есть спорная область — её и разберём.

§2Спорный случай: новое поле в ответе

В ответ добавлено поле priority, о котором старые клиенты не знают. Что произойдёт с ними, зависит от того, как они разбирают ответ.

было
{ "id": "a8d0…", "title": "Не работает VPN", "status": "open" }
стало
{ "id": "a8d0…", "title": "Не работает VPN", "status": "open", "priority": "high" }
Не сломается, если

клиент читает нужные ему поля и пропускает остальные. Так работает большинство разборщиков JSON по умолчанию.

Сломается, если

клиент проверяет ответ на строгое соответствие схеме и падает на незнакомом поле; или сохраняет ответ целиком туда, где есть ограничение на состав полей.

Обе ситуации встречаются на практике, поэтому крупные компании пришли к разным выводам и записали их в своих правилах.

§3Два ответа: GitHub и Azure

GitHub относит добавление поля в ответ к совместимым изменениям и перечисляет его в списке того, что выпускается без новой редакции API.

Azure, Breaking changes guidelinesправила компании

«Teams MAY define backwards compatibility as their business needs require. For example, Azure defines the addition of a new JSON field in a response to be not backwards compatible.»

Перевод. Команды MAY определять обратную совместимость так, как требуют их задачи. Например, Azure считает добавление нового поля JSON в ответ не обратно совместимым изменением.

Документ Azure: добавление нового поля JSON в ответ объявлено несовместимым изменением
Тот же абзац на странице. Обратите внимание на первое предложение: правила прямо разрешают командам определять совместимость по-своему. Значит, расхождение допускается самими правилами и не является ошибкой одной из сторон.
ИзменениеGitHubAzure
Добавить поле в ответсовместимонесовместимо
Добавить необязательный параметр запросасовместимосовместимо
Удалить поле ответанесовместимонесовместимо
Что требуется от клиентаигнорировать незнакомые поля—

§4Почему оба правы

Различие объясняется тем, кто выступает клиентом этих API.

У GitHub

Миллионы независимых клиентов, которых нельзя ни собрать, ни обновить. Дешевле один раз потребовать «игнорируйте незнакомые поля» и дальше свободно добавлять поля.

У Azure

Клиенты — в основном генерируемые SDK и инструменты развёртывания, которые сверяют ответ со схемой строго. Новое поле для них — изменение модели, и его нужно провести через версию.

Отсюда следует, что совместимость определяет команда, а не само изменение. Правильное решение то, которое записано и одинаково применяется. Незаписанное решение хуже любого из двух вариантов, потому что каждый участник считает по-своему.

§5Политика учебного проекта

В проекте выбран подход GitHub, и выбор записан в docs/VERSIONING.md вместе с обязанностью клиента.

docs/VERSIONING.md · выдержка
В этом проекте принято правило GitHub, поэтому клиенты MUST игнорировать
неизвестные поля ответа.

Несовместимыми считаются и допускаются только в новой мажорной версии:
- удаление или переименование endpoint;
- удаление или переименование поля ответа;
- изменение типа поля;
- добавление обязательного параметра или обязательного поля запроса;
- удаление значения из enum;
- ужесточение требований аутентификации или лимитов.

Список короткий и закрытый. Спор «ломает или нет» сводится к вопросу «есть ли это в списке», а если случая в списке нет, список дополняют отдельным изменением, а не решают на ходу.

Последний пункт списка

Ужесточение лимитов — изменение, которое легко не заметить: описание операции не меняется ни на строку. Но клиент, который присылал 100 запросов в минуту, начнёт получать 429. Для него это поломка, поэтому в проекте она приравнена к несовместимым.

§6Проверка машиной: oasdiff

Проверять этот список вручную при каждом изменении ненадёжно. Для сравнения двух описаний OpenAPI есть инструмент oasdiff: он находит несовместимые изменения и перечисляет их.

Разберём на конкретном изменении. В схеме обращения поле title переименовали в subject — название показалось точнее. Заодно подняли версию контракта с 1.1.0 до 1.2.0: автор счёл изменение совместимым, ведь поле никуда не делось, а только назвалось иначе.

что изменилось в описании
   Ticket:
     required:
       - id
-      - title
+      - subject
     properties:
       id: { type: string, format: uuid }
-      title: { type: string, maxLength: 120 }
+      subject: { type: string, maxLength: 120 }

   info:
-    version: 1.1.0
+    version: 1.2.0

Теперь сравним новое описание с прежним:

терминал
docker run --rm -v $(pwd):/specs tufin/oasdiff breaking \
  /specs/base.yaml /specs/revision.yaml
настоящий вывод
4 changes: 4 error, 0 warning, 0 info

error [response-required-property-removed] at /specs/revision.yaml
  in API GET /api/v1/tickets
    removed the required property `items/items/title` from the response with the `200` status

error [response-required-property-removed] at /specs/revision.yaml
  in API POST /api/v1/tickets
    removed the required property `title` from the response with the `201` status

error [response-required-property-removed] at /specs/revision.yaml
  in API GET /api/v1/tickets/{ticket_id}
    removed the required property `title` from the response with the `200` status

error [response-required-property-removed] at /specs/revision.yaml
  in API PATCH /api/v1/tickets/{ticket_id}
    removed the required property `title` from the response with the `200` status

Разберём вывод по частям.

Переименование засчитано как удалениеresponse-required-property-removed: для клиента, который читает ticket["title"], поля больше нет. То, что рядом появилось поле с другим именем, его код не спасёт.почему ошибка
Одна правка — четыре ошибкиСхема Ticket используется в четырёх операциях, и каждая из них теперь обещает клиенту не то, что обещала. Человек, правя схему, помнит про ту операцию, которую открыл, и обычно забывает про остальные.охват
Номер версии ничего не изменилАвтор поднял 1.1.0 → 1.2.0, но MINOR несовместимое изменение не оправдывает. Такое переименование допустимо только в 2.0.0 с новым путём /api/v2.версия

Вывод по этому изменению: либо вернуть прежнее имя поля, либо выпускать новую мажорную версию, либо — если имя действительно неудачное — добавить subject рядом со старым title, объявить title устаревшим и удалить его позже, как описано в главе 5.3.

Ту же проверку выполняет сборка при каждом pull request — задача breaking-changes сравнивает описание из ветки с описанием в main и не даёт объединить изменение, если нашлись ошибки. Подробно про сборку — глава 6.4.

Страница действия oasdiff для GitHub Actions в каталоге marketplace
Готовое действие для сборки. В проекте используется oasdiff/oasdiff-action/breaking с закреплённой версией — по тому же правилу, что и библиотека ReDoc в главе 3.4: версия внешнего инструмента всегда фиксируется.

§7Чего oasdiff не видит

Инструмент сравнивает описания, а не поведение. Всё, чего нет в описании, для него не существует.

ИзменениеВидит ли oasdiffПочему
Удалено поле ответадаИзменилась схема в описании
Параметр стал обязательнымдаИзменился признак required
Изменился порядок элементов списканетПорядок в схеме не описан
Ужесточён лимит запросовнетЛимит не выражен в описании
Поле status стало возвращать новое значениетолько если перечисление объявленоБез enum сравнивать нечего
Изменился смысл поля при том же типенетСмысл — в тексте описания, а не в схеме

Отсюда два практических следствия. Первое: чем подробнее описание, тем больше находит машина — объявленное перечисление ловит ошибку, необъявленное не ловит ничего. Второе: проверка машиной не отменяет ревью человеком, и именно поэтому изменение контракта в проекте требует и зелёной сборки, и одобрения владельца.

Не всякое молчание — разрешение

При подготовке этого справочника oasdiff не счёл удаление устаревшей операции GET /tickets/open ошибкой — она была помечена deprecated. Для клиента, который ею пользуется, это тем не менее поломка. Вывод инструмента учитывают, но решение принимает человек.

§8Частые ошибки

Считать, что «совместимо» — объективное свойствоGitHub и Azure отвечают по-разному на один и тот же вопрос, и оба записали свой ответ.
Не записать политику совместимостиТогда каждый участник команды решает по-своему, и решение зависит от того, кто смотрит pull request.
Требовать от клиентов игнорировать незнакомые поля молчаТребование есть только тогда, когда оно написано в контракте.
Полагаться на oasdiff как на единственную проверкуЛимиты, порядок элементов и смысл полей он не сравнивает.
Ужесточать проверки «незаметно»Описание не изменилось, а клиент сломался — для него это breaking change.

§9Проверьте себя

  1. Почему в определении breaking change стоит слово «потенциально»?
  2. Приведите по три бесспорно ломающих и бесспорно безопасных изменения.
  3. Почему GitHub и Azure по-разному оценивают добавление поля в ответ? Свяжите ответ с тем, кто их клиенты.
  4. Какую обязанность накладывает на клиентов политика учебного проекта и где она записана?
  5. Почему удаление одного поля из схемы дало в выводе oasdiff четыре ошибки?
  6. Назовите три изменения, которые oasdiff не поймает, и объясните, почему.
Определениеможет сломать интеграциюпотенциально, не фактически
Спорный случайновое поле в ответеGitHub — можноAzure — нельзя
Правилополитику записатьсписок закрытый
Проверкаoasdiff breakingзадача breaking-changes в CIплюс ревью человека

Справочник «Стандартизация HTTP API» · модуль 5 · глава 5.2Макаров Максим Николаевич

Кадр