{ "index": 112, "slug": "editorial-2024-11-field-deprecation", "title": "Как удалить устаревший API и не сломать последнего клиента", "excerpt": "Депрекация не доказывает, что endpoint больше не используют. Разбираем removal gate: как отделить активного клиента от неизвестной зоны, выбрать проверку, остановить удаление и определить готовность изменения.", "contentHtml": "
Команда пометила GET /v1/orders/{id} устаревшим, выпустила замену и увидела ноль вызовов в выбранном графике. Удаление кажется безопасным, пока внешний интегратор не получает 404 или 410. Такой сбой начинается не с плохого HTTP-кода, а с неверного вывода: отсутствие записей в одном источнике приняли за отсутствие всех потребителей.
Удаление API нужно рассматривать как доказательство с ограниченной областью, а не как дату в календаре. Сначала зафиксируйте одну операцию, затем свяжите известные вызовы с владельцами, явно запишите слепые зоны и только после этого откройте отдельный removal review. Если остался активный или неизвестный потребитель, автоматическое удаление должно остановиться.
\nВ OpenAPI 3.1.0 поле deprecated: true объявляет операцию устаревшей и рекомендует потребителям перейти с неё. Это часть описания контракта. Поле не читает access log, не знает о старых версиях SDK и не подтверждает, что replacement поддерживает тот же сценарий. Если генератор документации не публикует это поле или использует другую версию OpenAPI, поведение нужно проверить в конкретном toolchain.
Заголовок Sunset решает другую задачу. RFC 8594 описывает его как сигнал о том, что URI, вероятно, станет недоступен в указанный момент. Документ прямо называет timestamp подсказкой: после этой даты возможны ошибки, редирект или полная недоступность, но точное поведение не обещано. Поэтому Sunset помогает потребителю спланировать миграцию, но не заменяет inventory и проверку владельца.
После удаления ответы тоже нужно трактовать аккуратно. RFC 9110 описывает 404 как отсутствие текущего представления или нежелание раскрывать его существование. 410 означает, что доступ больше недоступен и состояние, вероятно, постоянно. Если сервер не знает, что удаление окончательно, RFC рекомендует 404. Ни один статус сам по себе не доказывает, что все клиенты перешли на новый маршрут.
\nФраза «удаляем v1» слишком широкая для решения. В одной версии могут жить разные методы, форматы ошибок и права доступа. Для removal gate запишите method, URI template, operationId, версию схемы, replacement, владельца и дату, после которой разрешён отдельный change. Если меняется только поле ответа, проверяйте поле, а не весь маршрут.
Сравните не только URL. Для каждого обязательного сценария зафиксируйте authentication boundary, коды ошибок, pagination, идемпотентность, лимиты и правила повторной отправки. Клиент может успешно получить 200 от нового маршрута и всё равно сломаться, если новый контракт иначе трактует пустой результат или отказ в доступе.
| Наблюдение | Что оно доказывает | Чего не доказывает | Следующее действие |
|---|---|---|---|
deprecated: true в OpenAPI | Операция объявлена устаревшей | Потребители мигрировали | Опубликовать replacement и начать consumer map |
| В графике нет запросов | В выбранном scope вызовы не наблюдались | Вызовов нет вообще | Записать период, sampling, регион и credential scope |
| Все найденные rows migrated | Названные потребители имеют путь перехода | Список потребителей полный | Проверить unknown-зону и назначить остаточный риск |
| Новая операция отвечает 200 | Один проверенный запрос прошёл | Совместимы ошибки, права и редкие сценарии | Сравнить фиксированный набор contract cases |
| Назначена дата Sunset | Есть объявленная граница планирования | Ресурс станет недоступен точно в эту дату | Согласовать отдельный removal change и stop condition |
Строка consumer map должна связывать потребителя не с догадкой, а с evidence. Минимальный набор полей: имя потребителя, состояние, источник, период, охват, владелец, replacement и следующий шаг. Состояние active означает наблюдаемый вызов старого контракта. migrated означает, что названный потребитель перешёл и это подтверждено проверкой. unknown означает, что область не наблюдается или не проверена.
Unknown — не мягкая форма migrated. Если telemetry не включает внешний tenant, старые credentials, редкий cron или трафик через общий gateway, строка должна остаться неизвестной. Запись «usage = 0» без периода и границы выглядит точной, но не позволяет воспроизвести вывод. За unknown назначают владельца остаточного риска: он либо расширяет разрешённую проверку, либо оставляет совместимость, либо выносит принятие риска на явное human review.
\nРазделите решение на три ветки. Любой active блокирует удаление: сначала нужен владелец и согласованный путь миграции. Любой unknown блокирует автоматическое удаление: неизвестность не равна нулевой активности. Если все названные строки migrated, можно открыть ручной review, но это ещё не разрешение удалить маршрут. В review должны попасть контракт, owner, notice, residual risk и план проверки после релиза.
Простейшее правило можно повторить локально на фиксированном наборе состояний. Команда ниже ничего не читает из production и не делает сетевых запросов: она только показывает, что unknown должен привести к блокировке.
\nnode --input-type=module -e \"const evidence = [{ state: 'migrated' }, { state: 'unknown' }]; const blocked = evidence.some(function (row) { return row.state === 'active' || row.state === 'unknown'; }); console.log(blocked ? 'block-removal' : 'human-review');\"\n\n// Ожидаемый вывод:\n// block-removal\n\nВ учебной записи один известный сервис мигрировал, но внешняя зона не проверена. Поэтому автоматический результат — block-removal. Поле blindZone объясняет причину отказа. В реальном проекте замените фиктивные rows на записи из разрешённых источников и сохраните ссылку на запрос, dashboard или commit, который можно открыть повторно.
operationId и URI, затем проверьте request, response, ошибки, auth и replacement. Для локального репозитория начните с команды rg -n 'getOrderV1|/v1/orders' .. Поиск показывает строки, но не доказывает runtime-вызов.SELECT client_id, count(*) FROM api_access WHERE route = '/v1/orders/{id}' AND ts >= :from AND ts < :to GROUP BY client_id; нужно адаптировать к своей схеме; результат без описания охвата остаётся частичным.Sunset, явно укажите scope и трактуйте дату как сигнал, а не гарантию доступности или удаления.До удаления назовите последний совместимый контракт и способ временно вернуть обработчик. Если новый код уже изменяет данные, восстановление HTTP-маршрута не вернёт прежнее состояние. Тогда граница восстановления должна включать совместимый read path, миграцию данных или заранее подготовленный обратный план.
\nСогласуйте, кто может остановить rollout и какой сигнал это делает. Например, рост ответов 4xx от известного клиента после удаления — достаточное основание остановить change, но не доказательство, что причина именно в API. Нужны correlation id, журнал изменения и сравнение с baseline. Откат должен вернуть систему в проверяемое состояние, а не просто скрыть симптом.
\nПроверка после релиза должна охватить старый и новый путь в заявленной области. Для старого маршрута проверьте ожидаемый статус и тело ошибки тестовым клиентом, не используя реальные секреты. Для нового — повторите контрактные сценарии и убедитесь, что авторизация, лимиты и ответы соответствуют согласованному replacement.
\nЗатем посмотрите на реальные сигналы: ошибки по client identity, retry rate, latency, gateway responses, очереди и обращения владельцев. Учитывайте задержку доставки логов и кэш. Если removal change коснулся только одного региона или credential scope, честный итог звучит так: «в заявленной области старый путь недоступен». Формулировка «клиентов не осталось» требует более сильного доказательства, чем обычно может дать один график.
\nПолного доказательства отсутствия всех consumers обычно не существует. Редкий cron не попадёт в короткое окно. Динамический URL может исчезнуть из статического поиска. Общий gateway скроет исходного клиента. Другой tenant, регион, credential или sampling оставит blind zone. Кэш, retry и очередь сдвинут вызов за пределы момента проверки.
\nПоэтому removal gate — это контроль риска, а не математическое доказательство. Он работает, когда у команды есть право читать нужные источники и назначать владельцев. При отсутствии доступа состояние остаётся unknown. Учебные rows и команда выше не являются фактическими данными, incident report или результатом запроса к вашему API.
Операция готова к отдельному removal change, если одновременно выполнены пять условий: scope операции и replacement однозначны; OpenAPI и уведомление опубликованы; каждая известная зависимость имеет owner и состояние; неизвестные области перечислены с периодом и stop condition; restore boundary проверена на совместимом контракте. Для каждого пункта оставьте ссылку на evidence.
\nЕсли хотя бы один пункт не выполнен, решение не должно превращаться в «удалим и посмотрим». Оставьте старую операцию, ограничьте новые зависимости и назначьте следующий проверяемый шаг. Депрекация тогда становится управляемым переходом, а не бессрочной пометкой, которая однажды внезапно превращается в 404.
\ndeprecated у Operation Object; поле объявляет операцию устаревшей, но не предоставляет список её потребителей.Sunset, его scope и ограничение: timestamp является подсказкой о возможной недоступности, а не гарантией.