Ломающие изменения API с объяснениями
Последнее обновление:
Ломающее изменение API — это любое изменение опубликованного контракта API, которое может заставить клиента, корректно написанного против старого контракта, отказать против нового — при том что сам клиент ничего не менял. Это единственное предложение и есть вся теория; всё остальное в этой статье — её следствия: какие изменения считаются, какие знаменитые ловушки выглядят безобидно, но таковыми не являются, как проверять повышение спецификации до её развёртывания и как стратегии версионирования покупают клиентам время. Чтобы применить это к двум настоящим документам, запустите OpenAPI Diff на старой и новой спецификациях и читайте вердикты рядом с этой страницей.
Контракт — это обещание о будущем
Спецификация API — это не документация в обыденном смысле; это обещание. Когда спецификация объявляет, что GET /pets принимает необязательный параметр limit и отвечает 200 объектом, содержащим строку name, каждый автор клиента строит ровно на этом: параметр опускается, когда не нужен, статус ветвит логику, поле читается как строка. Корректность клиента судят по обещанию, поэтому определение ломающего асимметрично — изменение ломает, если корректный старый клиент может отказать, даже если большинство клиентов случайно выживает. Одного строгого SDK, сгенерированного из старой спецификации и отказавшего на изменённом типе, достаточно: было нарушено обещание, а не клиент.
Эта рамка разрешает большинство споров о том, «считается» ли изменение. Удаление конечной точки ломает, потому что обещание включало её существование. Добавление конечной точки — нет, потому что ни один старый клиент ничего себе не обещал о путях, которые никогда не вызывает. Новое необязательное поле ответа безопасно, потому что старые парсеры игнорируют неизвестные поля: в любом здравом формате аддитивный вывод не может сделать читателя недействительным. А вот новая обязательная входная величина дотягивается до каждого существующего запроса и делает недействительными те, что её опускают. Тест всегда один: проиграйте вчерашний допустимый трафик против завтрашних правил и спросите, что теперь отказывает.
Два следствия важны на практике. Во-первых, контракт — это то, что опубликовано, а не то, что сервер случайно делает сегодня: недокументированное поле, которое клиенты открыли, вынюхивая трафик, не обещано, и его удаление грубо, но строго говоря не ломает — что является аргументом документировать ровно то, что вы намерены сохранять, и не более. Во-вторых, контракт включает всю поверхность стандартов, на которых он стоит: семантика методов и статусов HTTP, закреплённая в RFC 9110, — часть обещания, поэтому переопределение того, что означают 200 или 404, есть изменение контракта, даже если файл спецификации никогда не упоминает значения статусов.
Классические ловушки
Самый частый производственный разрыв — это добавление обязательного поля. Команда добавляет поле в тело запроса, помечает его обязательным, потому что новое бизнес-правило в нём нуждается, и выпускает — после чего каждый существующий клиент, из которых ни один не отправляет это поле, начинает получать 400. Безопасные версии скучны: добавьте поле как необязательное с серверным значением по умолчанию или требуйте его только на новой конечной точке. Вторая ловушка — удаление значений перечисления: значение статуса, категория, порядок сортировки упраздняются, потому что новый интерфейс их больше не предлагает, а сохранённые клиентские данные, мобильные приложения на старых сборках и партнёрские интеграции продолжают отправлять их годами. Значения перечисления стоят почти ничего, чтобы продолжать их принимать; отзыв одного следует трактовать как удаление конечной точки.
Изменения типов — третья ловушка, и они приходят в маскировках сужения и расширения. Сужение — строка в перечисление, целое в меньший диапазон, свободный объект в фиксированную форму — отклоняет ввод, который раньше был допустим, и ломает на первый взгляд. Расширение тоньше: изменение поля ответа со строки на строку-или-null или целочисленного id на строковый выглядит щедро и ломает каждый строгий парсер и сгенерированный SDK, построенный для старого типа. На стороне ответа есть зеркальная ловушка: поле, которое всегда присутствовало, становится необязательным. Клиенты читают его безусловно; первый ответ, который его опускает, — это нулевой указатель в чужой кодовой базе.
Четвёртая ловушка семантическая, и ни один differ её не поймает: поле сохраняет имя и тип и меняет смысл. Цена, молча переключающаяся с центов на доллары, метка времени, переходящая с UTC на местное время, список, который был отсортирован и больше не отсортирован, — diff спецификации пуст, а каждый клиент неправ. Семантические разрывы — причина того, что механический diff является полом, а не потолком для ревью: он расчищает изменения, которые никому не могут навредить, чтобы человеческое внимание сосредоточилось на изменениях, которые просто могли бы.
Проверка повышения спецификации поставщика перед развёртыванием
Поставщики постоянно публикуют новые версии спецификаций, и «поднять SDK и передеплоить» — это то, как тихие разрывы достигают продакшена. Дисциплинированная проверка занимает пятнадцать минут. Во-первых, получите оба документа — спецификацию, против которой ваш деплой реально работает сегодня, и кандидатную — и сравните их механически вместо чтения журнала изменений, потому что журналы изменений — маркетинговые документы: они перечисляют функции, а не удаления. OpenAPI Diff производит этот список локально, что важно, когда API внутренний и его спецификация не должна вставляться в чужой хостящийся инструмент.
Во-вторых, пройдите по списку ломающих изменений как по чек-листу против вашего собственного использования. Большинство разрывов поставщика для вас теоретические: удалённая конечная точка, которую вы никогда не вызываете, новое обязательное поле на ресурсе, который вы никогда не создаёте. Опасные записи — те, что пересекаются с вашим трафиком, — поэтому проверке нужен ваш инвентарь вызовов, а не только diff. В-третьих, проверьте недокументированную поверхность: статусы, по которым вы ветвитесь, формы ошибок, которые вы парсите, заголовки, которые вы читаете. Та же привычка, что защищает потребителей вебхуков — запинить и тестировать контракт полезной нагрузки, как в подписях вебхуков с объяснениями, — применима к любому контракту, который контролирует кто-то другой.
Наконец, вводите по этапам. Направьте стейджинг-клиента на новую версию, проиграйте записанные производственные запросы (санитизированный захват, обращённый так, как предписывает руководство по HAR) и сравните ответы раньше любого пользователя. Если поставщик предлагает окно устаревания или пин версии, берите его: самое дешёвое ломающее изменение — то, которое вы принимаете в своём темпе, после готовности ваших клиентов, а не в день релиза поставщика.
Стратегии версионирования: URL, заголовок или не ломать никогда
Классический ответ на ломающее изменение — идентификатор версии, и классический спор — куда его поместить. Версионирование по URL (/v1/, /v2/) явно, дружелюбно к кэшу, тривиально маршрутизируемо и видно в каждой строке лога; его цена — оно версионирует весь API сразу, поэтому одна ломающая конечная точка тащит каждого клиента через миграцию. Версионирование по заголовку — заголовок версии или датированная схема согласования содержания — сохраняет URL чистыми и может версионировать отдельные ресурсы ценой невидимости: версия исчезает из логов, ссылок и адресных строк браузера, и отладка становится археологией. Ни то ни другое не ошибочно; индустрия просто охотнее платит цену URL, потому что операбельность в три часа ночи побеждает элегантность.
Более глубокая стратегия — редко нуждаться в версиях. Только аддитивная эволюция — никогда не удалять, никогда не требовать, никогда не менять типы; только добавлять необязательное — сохраняет одну версию живой неограниченно долго, и именно так на деле работают самые долгоживущие публичные API. Она требует дисциплины заранее: щедрой толерантности на вводе, id, типизированных как строки с первого дня (чтобы диапазон мог расти), перечислений, спроектированных для расширения, и процесса устаревания, который предупреждает задолго до отзыва чего-либо. Семантическое версионирование, как оно кодифицировано на semver.org, даёт сигнальный слой, который собственная спецификация OpenAPI Initiative сознательно оставляет издателям: повышение мажорной версии объявляет разрыв, и клиенты могут запиниться против него.
Какая бы стратегия ни была выбрана, она работает только как социальный контракт, подкреплённый механикой: версии должны действительно работать бок о бок в течение заявленного окна, устаревания должны объявляться там, где их увидят авторы клиентов (журнал изменений, заголовки ответа, прямое уведомление для крупных потребителей), а использование старой версии должно измеряться, чтобы «её больше никто не использует» было фактом, а не надеждой. Версия, которая опубликована, но не может работать параллельно, — не стратегия; это переименование разрыва.
Где место differ
Механический differ зарабатывает своё место в два момента. В ревью он превращает «что-нибудь сломалось?» из послеполуденного чтения бок о бок в список, который можно вычёркивать, — и, что столь же полезно, он доказывает отрицание: когда релиз утверждает, что он только аддитивный, пустой список ломающих изменений — это свидетельство, а не ощущения. В непрерывной интеграции то же сравнение может запускаться на каждом запросе на слияние, касающемся спецификации, и рушить сборку, когда ломающее изменение приходит без повышения версии или одобрения, требуемых политикой проекта. Таблица классификации — это политика; инструмент просто отказывается позволить ей быть забытой к дедлайну.
Его ограничения определяют остальной рабочий процесс. Differ читает только объявленный контракт: он не может видеть поведение, производительность, семантику или недокументированные причуды, от которых зависят клиенты, и он суммирует патологически глубокие схемы вместо того, чтобы разворачивать их вечно. Поэтому полные ворота — это конвейер, а не страница: механический diff для контракта, контрактные тесты, проигрывающие настоящие клиентские вызовы, для поведения, поэтапный ввод для семантики и мониторинг на день, когда все три что-то пропустят. Используемый так, diff — самый дешёвый из четырёх слоёв: пятнадцать секунд, которые удаляют самый неловкий класс отказа — тот, где разрыв был всё время записан в спецификации, и никто не сравнил документы.
Именно за эту привычку на самом деле агитирует эта статья. Спецификации дёшево хранить и ещё дешевле сравнивать; инциденты — нет. Храните каждую опубликованную версию каждого контракта, которым владеете или который потребляете, сравнивайте каждого кандидата с версией в продакшене до его выпуска и трактуйте список ломающих изменений как настоящую повестку релиза. Команды, которые делают это, перестают спорить о том, «должно ли было» изменение кого-то сломать — у них есть список, датированный и заархивированный, с момента до выпуска.