Изменения API

Breaking changes: что считается и как выпустить

8 мин чтения обновлено

Breaking change это изменение, которое корректно написанный вызывающий не смог бы пережить. Определение важно, потому что большинство споров о том, «считается» ли что-то, на самом деле споры о том, кто держал это неправильно. Если вызывающий следовал вашей документации, а ваше изменение заставило его код перестать работать, изменение было breaking. Ваши намерения к этому не имеют никакого отношения.

Это весь тест. Остальная часть этой статьи посвящена тому, что из него следует: что его не проходит, что проходит, как поймать провал до слияния и что делать, как только вы знаете, что выпускаете такое.

Что считается breaking change?

Применяйте тест к вызывающему, а не к диффу. Изменение является breaking, когда вызывающий, полагавшийся только на документированное поведение, должен изменить свой код, конфигурацию или данные, чтобы продолжить работать. Удаление поля, переименование endpoint, ужесточение валидации, изменение значения по умолчанию и изменение типа значения, всё это квалифицируется. Добавление опционального поля, нет. Исправление бага обычно нет, с одним важным исключением ниже.

ИзменениеBreaking?Почему
Удаление или переименование поля, endpoint, флага или опцииДаКорректные вызывающие ссылаются на это
Добавление опционального поля или нового endpointНетСуществующие вызовы не меняются
Превращение опциональной входной величины в обязательнуюДаВызовы, опускавшие её, теперь падают
Ужесточение ранее принимавшейся валидацииДаВходные данные, которые работали, теперь отклоняются
Изменение значения по умолчаниюДаВызывающие, не установившие его, получают новое поведение
Изменение типа (строка в число, единичное значение в массив)ДаПарсеры, написанные под документированный тип, падают
Изменение порядка ключей объектаНетЕсли только вы не документировали порядок
Исправление бага, на который полагались вызывающиеНа практике даСм. раздел о случайных контрактах
Повышение лимита частоты или размераНетНичто из работавшего не перестаёт работать
Понижение лимита частоты или размераДаТрафик, который был в порядке, теперь ограничивается
Изменение формулировки сообщения об ошибкеЗависитBreaking, если вы это документировали или вызывающие сопоставляют с ним

Что не является breaking change?

Изменение не breaking, когда каждый вызов, который работал раньше, по-прежнему работает без изменений и значит то же самое. Новый endpoint, опциональный параметр запроса, новое поле в ответе, превращение обязательного входного значения в опциональное, повышение лимита и улучшение сообщения об ошибке, с которым никто не сопоставляет, проходят тест. Такие аддитивные изменения можно выпускать в минорном релизе с обычной записью в changelog.

Аддитивные изменения всё же ломают вызывающих в трёх случаях. Клиент, чей десериализатор отклоняет неизвестные поля, падает на первом же новом поле ответа, поэтому заранее документируйте, что вызывающие должны игнорировать незнакомые поля. Новое значение enum ломает каждого вызывающего с исчерпывающим switch (подробнее ниже). А растущий ответ может вытолкнуть вызывающего за лимит размера, таймаут или ширину столбца, о которых ему не приходилось думать.

Четыре строки таблицы заслуживают более пристального взгляда, потому что именно там возникают разногласия.

Четыре breaking change, которые упускают команды

Случайные контракты. Если ваш API три года возвращал одно и то же недокументированное поле, вызывающий на это опирался. Закон Хайрума: короткая версия: при достаточном числе пользователей каждое наблюдаемое поведение вашей системы будет от кого-то зависимым. Вот почему «это было исправление бага», не защита. Исправление может быть корректным и всё равно быть breaking. Выпустите его как таковое.

Изменения поведения без изменения схемы. Поле всё ещё там, тип тот же, а значение теперь означает что-то другое. status, который был active или inactive, а теперь также возвращает suspended, ломает каждого вызывающего с исчерпывающим switch. Timestamp, переходящий с локального времени на UTC, ломает всех, кто не прочитал документацию дважды. Ничто в диффе файла OpenAPI этого не показывает.

Ужесточённая валидация. Вы начинаете отклонять email без TLD, или пробелы в конце, или имена длиннее 80 символов. Каждый вызывающий, отправлявший ровно это, теперь получает 400 на запрос, который работал на прошлой неделе. Изменения валидации чаще всего выпускаются как исправление «усиления».

Изменённые значения по умолчанию. Никто, кто установил значение явно, ничего не замечает. Все, кто этого не сделал, а это большинство вызывающих, получают новое поведение, не меняя ни строки. Изменённое значение по умолчанию ломает большинство ваших пользователей именно потому, что они никогда не видели эту настройку.

Как обнаружить breaking change до выпуска?

Сравните контракт в pull request с контрактом в основной ветке, в CI, и провалите сборку при breaking-различии. Инструменты сравнения схем есть для большинства форматов интерфейсов, и каждый знает правила breaking для своего формата:

ИнтерфейсИнструментЧто сравнивает
REST (OpenAPI)oasdiffДве спецификации OpenAPI, с отчётом о breaking changes
gRPC (Protobuf)buf breakingФайлы .proto, на уровне wire или исходного кода
GraphQLGraphQL InspectorДве схемы, с пометкой breaking и опасных изменений
Rust cratescargo-semver-checksПубличный API против последней опубликованной версии
Пакеты TypeScriptAPI ExtractorЗакоммиченный отчёт о публичном API пакета

Эти инструменты надёжно ловят удалённые поля, переименованные операции и изменённые типы. Они не видят первые два из четырёх видов выше, случайный контракт и изменение поведения, потому что ни то, ни другое не отражается в схеме. Используйте инструмент, чтобы остановить очевидные случаи, а для остальных вопрос на ревью: «заметит ли это корректный вызывающий?». Тот же CI-джоб, естественное место, чтобы требовать запись в changelog, как описано в проверке записей changelog в CI, а изменения API gRPC и Protobuf разбирают случаи на уровне wire.

Как пометить breaking change в коммите?

В Conventional Commits breaking change помечается ! перед двоеточием (feat(api)!: remove the legacy export endpoint) или футером, начинающимся с BREAKING CHANGE: и описанием. Любой из вариантов соответствует major версии. Пишите футер как первый черновик записи в changelog: кого это затрагивает и что им нужно сделать. Conventional commits и changelog разбирает, как далеко вас доводит это соглашение.

То же правило действует для библиотек. Удалённая публичная функция, суженный тип параметра или изменённое возвращаемое значение, это major версия по семантическому версионированию. Библиотеки соблюдают его не всегда: исследование 119 879 обновлений в Maven Central показало, что 16,6% нарушили семантическое версионирование, но затронули лишь 7,9% клиентских проектов, потому что большинство этих изменений касалось кода, который ни один клиент не вызывал. Поломку измеряют у вызывающего.

Как выпускается breaking change?

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

  1. Решите, является ли это им. Используйте тест выше, а не дифф. Если два инженера не согласны, это breaking; несогласие, доказательство того, что вызывающий мог разумно полагаться на старое поведение.
  2. Версионируйте это. По семантическому версионированию breaking change, это major версия. Если вы работаете с датированным или версионированным API, это идёт в новую версию, а старая продолжает работать до объявленной даты. Если вы не можете версионировать, вы не выпускаете breaking change, вы выпускаете сбой с записью в changelog. Какая схема несёт версию, тема лучших практик версионирования API.
  3. Напишите запись до объединения кода. У записи фиксированная форма: что меняется, кого затрагивает, что им нужно сделать, и до когда. Если вы не можете заполнить все четыре, изменение не готово. Шаблон release notes ставит эти записи первыми, с датой вместо номера версии, именно поэтому.
  4. Дайте дедлайн, а не номер релиза. «Удалено в v5» ничего не значит для того, кто не следит за вашими релизами. «Перестаёт работать 1 ноября 2026 года» значит одно и то же для всех.
  5. Предоставьте миграцию. Пример кода старого вызова рядом с новым. Если изменение переименование, назовите оба имени в одном предложении. Если это удалённое поле, скажите, куда делись данные.
  6. Объявите везде, где было документировано старое поведение. Changelog, страницу документации, описывающую endpoint, release notes SDK, и заголовок депрекации в ответе, если он у вас есть. Объявленное в одном месте объявлено людям, которые случайно туда посмотрели.
  7. Замкните петлю. Если клиентка попросила изменение, или сообщила о баге, который к нему привёл, скажите ей, когда оно выпущено. Это шаг, превращающий это из чего-то, сделанного с вашими пользователями, в что-то, сделанное вместе с ними.

Как выглядит хорошая запись о breaking change?

Хорошая запись называет затронутого вызывающего в первой строке, объявляет дату, и включает исправление. Вот одна для случая ужесточённой валидации, в форме, которую мы используем:

Адреса электронной почты без домена отклоняются с 1 ноября 2026 года. POST /users и PATCH /users/:id в настоящее время принимают значения email вроде alice@localhost. С 1 ноября они возвращают 400 invalid_email. Затрагивает любую интеграцию, создающую пользователей из внутренних каталогов. Миграция: отправьте полностью квалифицированный адрес, или опустите поле и установите его позже. Никаких изменений не требуется, если ваши адреса уже имеют домен, что верно для 99,4% аккаунтов, созданных в этом году.

Где место этому уведомлению, и что ещё должно быть рядом с ним, разбирает changelog API.

Процент в конце, не украшение. Он говорит читательнице, стоит ли ей беспокоиться, что и есть вопрос, с которым она открыла запись.

Почему бы просто не избегать их?

Потому что альтернатива хуже. API, который никогда ничего не ломает, накапливает каждую ошибку, которую когда-либо совершил: неправильно названное поле, неверное значение по умолчанию, timestamp в локальном времени. Каждая из них, налог на каждого нового вызывающего навсегда, чтобы защитить вызывающих, которые могли бы мигрировать за один вечер. Команды с лучшей репутацией стабильности ломают вещи редко, по расписанию, с путём миграции и предупреждением, дошедшим до людей, для кого оно предназначалось.

Механика этого предупреждения, тема сопутствующей статьи о депрекации API. Запись, объявляющая это, составляется так же, как любая другая запись в ленте changelog: из объединённого pull request, удержанная для человека, затем опубликованная там, где затронутые вызывающие уже читают.

FAQ

В чём разница между breaking и non-breaking изменением? Breaking change заставляет корректного вызывающего менять код, конфигурацию или данные, чтобы продолжить работать. Non-breaking оставляет каждый существующий вызов рабочим с тем же значением, поэтому добавления обычно безопасны, а удаления, переименования и ужесточённые правила обычно нет.

Считается ли добавление обязательного поля? Да. Каждый существующий вызов его опускает, поэтому каждый существующий вызов теперь падает. Добавьте его как опциональное с разумным значением по умолчанию, или версионируйте endpoint.

Считается ли исправление бага? Может быть. Если вызывающие полагались на ошибочное поведение, исправление его ломает их, независимо от того, что говорила документация. Относитесь к любому исправлению, меняющему наблюдаемый вывод, как к breaking, если только вы не можете показать, что никто на него не полагался.

Применяется ли семантическое версионирование к веб-API? Правило да: breaking change получают новую major версию, а старая продолжает работать в течение объявленного периода. Номер часто живёт в URL или заголовке даты, а не в версии пакета.

Сколько предупреждения достаточно? Достаточно, чтобы вызывающий нашёл уведомление и выполнил работу. Девяносто дней, обычный минимум для публичных API; дольше для всего, используемого в коде, отправляемом конечным пользователям и не обновляемом удалённо.


Технические утверждения в этой статье не проходили независимую проверку. Если здесь что-то не так, сообщите нам, и мы исправим.

По теме на changeloop: Шаблон релиз-нот, Документация для разработчиков

changeloop
Команда, которая делает changelog, замыкающий цикл. Пользователи о чём-то просят, ваша команда это делает, тот, кто просил, узнаёт об этом.