Обратная совместимость и deprecation

Урок о том, как развивать API годами, не ломая клиентов, которых вы не контролируете.

Обратная совместимость — это свойство нового релиза API, при котором клиенты, написанные под старую версию, продолжают работать без изменений.

Версионирование (урок 1) — это тяжёлая артиллерия: новая мажорная версия стоит дорого и вам, и клиентам. В идеале к ней прибегают редко, а большинство изменений вкатывают совместимо, вообще без новой версии. Чтобы так уметь, нужно чётко понимать, какие изменения безопасны, а какие — нет, и как корректно выводить из эксплуатации то, что всё же приходится ломать.

Что ломает совместимость

Ломающим считается любое изменение, после которого корректный старый клиент начинает работать неправильно. Типичные виновники:

  • Удаление поля из ответа — клиент, который его читал, получает undefined/null.
  • Переименование поля (namefull_name) — для клиента это удаление одного и добавление другого.
  • Сужение типа — было число или строка, стало только число; был объект с тремя ключами, стало с двумя.
  • Превращение необязательного в обязательное — старые запросы без этого поля начинают отклоняться.
  • Изменение семантики при том же имени — поле status раньше было "ok"/"fail", стало числом. Самый коварный случай: схема как будто та же, а смысл другой.
  • Ужесточение валидации — раньше принимали, теперь 400.

Что безопасно: аддитивные изменения

Совместимы изменения, которые добавляют, не трогая существующего:

  • Новое поле в ответе (клиент его не ждёт и игнорирует).
  • Новый необязательный параметр запроса со значением по умолчанию.
  • Новый эндпоинт или новый метод на существующем ресурсе.
  • Новое значение в перечислении — условно безопасно (см. толерантного читателя ниже).

Правило-памятка:

Совместимо (аддитивно)Ломает
добавить поле в ответудалить/переименовать поле
добавить необязательный параметрсделать параметр обязательным
добавить эндпоинтсузить тип значения
расширить enum (осторожно)сменить смысл существующего поля

Толерантный читатель (tolerant reader)

Половина дела — на стороне сервера, половина — на стороне клиента. Принцип толерантного читателя гласит: клиент должен читать только то, что ему нужно, и игнорировать всё остальное. Не падать на незнакомых полях, не валить ответ из-за нового значения в enum, не требовать строгого порядка ключей. Это прямое следствие принципа надёжности Постела: «будь строг к тому, что отправляешь, и терпим к тому, что принимаешь».

Сервер прислал:
{ "id": 7, "name": "Аня", "badge": "new" }   <- поле badge добавили

Хрупкий клиент: падает, потому что схема "не совпала".
Толерантный клиент: читает id и name, badge просто игнорирует.

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

Процесс deprecation

Рано или поздно что-то всё же придётся убрать. Делается это не одномоментно, а через объявленный процесс устаревания. HTTP даёт для этого стандартные заголовки. Deprecation помечает, что эндпоинт/версия устарели, а Sunset называет дату, после которой они перестанут работать:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
curl -i https://api.example.com/v1/users/42
# в ответе видим заголовки Deprecation и Sunset

Полный процесс вывода из эксплуатации обычно такой:

  1. Объявить. Запись в changelog, заголовки Deprecation/Sunset, письмо известным потребителям.
  2. Дать миграционный период. Старое и новое работают параллельно — недели или месяцы, чтобы клиенты успели переехать.
  3. Предупреждать перед концом. Напоминания по мере приближения даты Sunset.
  4. Убрать. Только после Sunset и желательно с понятной ошибкой (410 Gone со ссылкой на новую версию), а не молчаливым 404.

Как работает под капотом

Заголовки Deprecation/Sunset — это просто метаданные ответа; сервер их выставляет на устаревших маршрутах, а зрелые клиенты читают и логируют, поднимая алерт, что пора мигрировать. Часто этого мало: API-провайдеры строят телеметрию использования — считают, кто из клиентов ещё дёргает устаревший эндпоинт, чтобы адресно достучаться до отстающих перед отключением. Миграционный период технически держится на сосуществовании: старый и новый код живут рядом, нередко старый превращается в тонкий адаптер поверх новой реализации. А changelog — это формальный контракт коммуникации: каждое заметное изменение фиксируется с датой и пометкой совместимости, чтобы у клиента был единый источник правды.

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

  • Удалять без предупреждения. Молча выпиленное поле или эндпоинт — самый быстрый способ сломать прод клиентам и потерять доверие.
  • Менять смысл, сохраняя имя. Опаснее явного слома: тесты на схему проходят, а поведение поехало. Лучше новое имя/поле.
  • Слишком короткий миграционный период. Внешним клиентам нужны недели и месяцы, не дни. Учитывайте релизные циклы партнёров.
  • Нет changelog. Без письменной истории изменений клиент не знает, что и когда поменялось, и не доверяет API.
  • Хрупкий клиент. Жёсткий парсинг, падающий на незнакомом поле, превращает безопасные аддитивные изменения в ломающие. Пишите толерантных читателей.

Итоги

  • Ломают совместимость удаление, переименование, сужение типа, ужесточение требований и смена семантики; добавление — безопасно.
  • Стремитесь вкатывать изменения аддитивно, оставляя новую мажорную версию на крайний случай.
  • Толерантный читатель: клиент игнорирует незнакомое — это делает добавление поля совместимым.
  • Устаревание — это процесс: объявить (Deprecation/Sunset, changelog) → миграционный период → предупреждения → удаление с понятной ошибкой.
  • Никогда не ломайте молча и не меняйте смысл поля под старым именем.
Проверьте себя
1. Какое из изменений сохраняет обратную совместимость?
AПереименование поля created в created_at
BДобавление нового необязательного поля в ответ
CПревращение необязательного параметра запроса в обязательный
DСужение типа поля с «строка или число» до «только число»
2. В чём суть принципа толерантного читателя (tolerant reader)?
AСервер обязан принимать любой формат запроса
BКлиент читает только нужные ему поля и игнорирует незнакомые, не падая на них
CAPI должен поддерживать все версии вечно
DКлиент валидирует ответ по строгой схеме и падает при отклонении
3. Какой HTTP-заголовок указывает дату, после которой устаревший эндпоинт перестанет работать?
AExpires
BRetry-After
CSunset
DCache-Control