Как deprecate’нуть и удалить endpoint
Пошаговая процедура корректного изъятия HTTP-endpoint’а из
публичного API без поломки mobile/web-клиентов, которые ещё его
используют. Actual reference по формату URL, middleware, error-
mapping — ../conventions/http-api.
Версионирование API — ../conventions/api-versioning.
Эта страница — процедура, от «решили выпилить» до «code
deleted».
Содержание
- Когда deprecate’нуть
- Таймлайн
- Фаза 1: announce
- Фаза 2: deprecation-headers и метрика
- Фаза 3: наблюдение использования
- Фаза 4: sunset (410 Gone)
- Фаза 5: удаление кода
- Переименование endpoint’а
- Что НЕ делать
- Чеклист
- Связанные разделы
Когда deprecate’нуть
Причины удалить endpoint:
- Endpoint дублирует более новый (
/v1/places/{id}/reviewsvs/v1/reviews?place_id=X). - Endpoint опирается на устаревшую модель данных (колонка будет дропнута по expand-contract).
- Endpoint невозможно обслуживать с нужным SLO.
- Endpoint имеет security-дыру, которую нельзя закрыть без breaking change (поменять формат ответа, убрать поле).
Не deprecate’ни, если:
- Endpoint работает корректно, но «не нравится» именем. Rename — отдельный паттерн, дешевле.
- Можно исправить non-breaking (добавить новое поле, опционально проигнорировать старый query-параметр).
- Нагрузка низкая. Endpoint без трафика не стоит усилий на deprecation cycle; оставь и задокументируй как «не развивается».
Таймлайн
Полный цикл — минимум 90 дней для публичного API, минимум 14
дней для internal (/internal/*) API:
| Фаза | Длительность (public) | Длительность (internal) |
|---|---|---|
| 1. Announce | день 0 | день 0 |
| 2. Deprecation headers + метрика | дни 0–90 | дни 0–14 |
| 3. Наблюдение | 30+ дней после последнего клиента | 7+ дней |
| 4. Sunset (410 Gone) | 30+ дней | 7+ дней |
| 5. Удаление кода | после sunset | после sunset |
Сокращать можно только через explicit approval lead-инженера, с документированной причиной (security-incident, legal-deadline). «Никто не использует» — не причина сокращать без observation.
Фаза 1: announce
Действия:
-
Issue в сервис-репо с тегом
deprecationи следующей информацией:- Endpoint (метод + путь + версия):
GET /v1/reviews/by-place/{id}. - Replacement (если есть):
GET /v1/reviews?place_id={id}. - Sunset date: конкретная дата, не «через 3 месяца».
- Причина: одна строка.
- Endpoint (метод + путь + версия):
-
Changelog entry в сервис-репо
CHANGELOG.md:## [Unreleased] ### Deprecated - `GET /v1/reviews/by-place/{id}` — use `GET /v1/reviews?place_id={id}` instead. Sunset: 2026-07-20. -
Уведомление клиентов. Для public API — запись в status-page / release-notes, которую читает mobile/web-команда. Для
/internal/*— сообщение в backend on-call канал с owner’ом downstream-сервиса. -
Обновление
../services-catalogи README сервиса, если endpoint описан публично.
До окончания Фазы 1 — endpoint работает как обычно (200 OK на валидные запросы, никаких deprecation-признаков в ответе).
Фаза 2: deprecation-headers и метрика
Ставим визуальные сигналы для клиентов и метрику для себя.
Sunset header (RFC 8594)
func (h *Handler) ByPlace(w http.ResponseWriter, r *http.Request) {
// RFC 8594: дата и время в HTTP-дате (IMF-fixdate).
w.Header().Set("Sunset", "Sat, 20 Jul 2026 00:00:00 GMT")
// RFC 8288: ссылка на replacement.
w.Header().Set("Link",
`<https://api.kazmaps/v1/reviews?place_id={id}>; rel="successor-version"`)
w.Header().Set("Deprecation", "true")
metrics.DeprecatedEndpointCalls.WithLabelValues(
"GET", "/v1/reviews/by-place/{id}",
).Inc()
// ... обычный handler-код ...
}Три заголовка:
Sunset— RFC 8594, конкретная дата выпила.Deprecation: true— draft RFC, но уже de-facto стандарт; gates инструментов типа Postman подсвечивают endpoint красным.Link: ...; rel="successor-version"— ссылка на замену. Клиенты, работающие по OpenAPI / curl, увидят и смогут переключиться.
Метрика
var DeprecatedEndpointCalls = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "deprecated_endpoint_calls_total",
Help: "Calls to endpoints marked for removal.",
},
[]string{"method", "route"},
)Alert на низкий уровень (не на zero):
- alert: DeprecatedEndpointStillUsed
expr: |
rate(deprecated_endpoint_calls_total[1h]) > 0
for: 24h
labels: { severity: ticket, team: backend }
annotations:
summary: "Deprecated endpoint {{ $labels.route }} still used"Срабатывает, если endpoint вызывали хотя бы раз за 24 часа — сигнал, что sunset-дату передвигать нельзя без потерь.
OpenAPI-документация
Если endpoint описан в OpenAPI-спеке сервиса:
paths:
/v1/reviews/by-place/{id}:
get:
deprecated: true
description: |
DEPRECATED: use `GET /v1/reviews?place_id={id}` instead.
Sunset: 2026-07-20.deprecated: true — стандарт OpenAPI 3, подсвечивается в Swagger UI
/ Redoc. Code-generators (oapi-codegen, openapi-generator) помечают
сгенерированные method’ы // Deprecated: ....
Логирование на стороне сервера
Опционально — WARN с user_agent при каждом вызове, чтобы
понять, какие клиенты ещё используют:
log.FromCtx(r.Context()).Warn("deprecated endpoint called",
"route", "/v1/reviews/by-place/{id}",
"user_agent", r.UserAgent(),
"client_ip", r.RemoteAddr)Не делай это INFO — зашумит логи.
Фаза 3: наблюдение использования
Smotrim deprecated_endpoint_calls_total и логи:
- Идентификация клиентов.
User-Agent(для mobile — обычно содержит version сборки). Если видишь старые версии (≥ 3 major позади) — норма, они будут обновляться естественным churn’ом. - Активные попытки уведомить. Для SaaS-клиентов (если есть) — email/dashboard notification. Для mobile — force-upgrade через минимальную версию (отдельная процедура у mobile-команды).
- Окно
rate > 0. Ждём минимум 30 дней с нулевого использования до sunset. Не «пропало за три дня, выпиливаем» — может быть temporary drop клиентского трафика.
Если usage не падает ниже порога — отодвигаем sunset date. Это не fail, это нормальная часть цикла; корректируем announce, issue, OpenAPI.
Фаза 4: sunset (410 Gone)
Endpoint не удаляется из кода; возвращает 410 Gone с пояснением:
func (h *Handler) ByPlace(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Deprecation", "true")
w.Header().Set("Sunset", "Sat, 20 Jul 2026 00:00:00 GMT")
w.Header().Set("Link",
`<https://api.kazmaps/v1/reviews?place_id={id}>; rel="successor-version"`)
writeError(w, http.StatusGone, "gone",
"this endpoint is no longer available; use GET /v1/reviews?place_id={id}")
}Почему 410 Gone, а не 404 Not Found:
- 410 = ресурс намеренно удалён; клиент должен прекратить запросы.
- 404 = ресурс не найден, клиент может думать «временный глюк» и retry’ить.
- 410 явно сигнализирует mobile-клиентам, что их версия устарела.
Длительность Фазы 4 — 30+ дней для public, 7+ для internal. Даёт клиентам последнюю возможность обновиться, увидев 410 в production, не в документации.
Alert на 410 оставляем — если rate не падает в ноль после первого 410-rollout’а, значит мы упустили класс клиентов. Тогда — rollback в статус «deprecated» (200 OK + warning), re-notify, продлить таймлайн.
Фаза 5: удаление кода
После 30+ дней 410 Gone с нулевым использованием:
-
PR: удалить handler, route, OpenAPI-спецификацию, тесты.
-
Удалить метрику и alert (
DeprecatedEndpointCalls,DeprecatedEndpointStillUsed). -
CHANGELOG entry:
### Removed - `GET /v1/reviews/by-place/{id}` (deprecated since 2026-04-20, sunset since 2026-07-20). Use `GET /v1/reviews?place_id={id}`. -
Update
../services-catalogи README. Endpoint больше не упоминается.
Не меняй версию API (/v1/) ради одного удаления. Версия API —
cross-resource контракт; удаление одного endpoint’а не требует
bump’а. Bump версии — только при breaking changes набора
endpoint’ов (см.
../conventions/api-versioning).
Не удаляй миграции БД в том же PR. Если endpoint был
единственным читателем колонки — отдельный expand-contract для
DROP COLUMN (см.
../conventions/db-pgx#expand-contract).
Переименование endpoint’а
Переименование = два endpoint’а одновременно, а не «rename в коде».
- Поднять новый endpoint (
GET /v1/reviews?place_id=X) с полностью повторяющей функциональностью. Обязательно: те же статусы, формат ответа, права доступа, validation rules. Тест integration’ом оба одновременно на одинаковых fixtures. - Announce deprecation старого (см. Фаза 1).
- Пройти Фазы 2–5 как обычно.
Не делай 301 Redirect со старого на новый как замену deprecation’а
— клиент-библиотека может не следовать редиректам (особенно на
non-GET), и часть трафика будет молча теряться.
Что НЕ делать
- Не удалять endpoint одним PR. Всегда через deprecation cycle. Удаление без announce — breaking change, который ловится пользователями, а не CI.
- Не менять формат ответа «по-тихому». Если нужно реструктурировать — новый endpoint + deprecate старый, не «добавим новое поле, удалим старое».
- Не оставлять endpoint в Phase 2 (deprecated) вечно. Deprecation должен закончиться или отменой, или sunset’ом. Вечный deprecated — debt.
- Не укорачивать таймлайн без одобрения lead’а. «Никто не использует за 3 дня наблюдения» — наблюдай 30.
- Не удалять метрику
DeprecatedEndpointCallsраньше, чем удалён handler. Пока handler в коде — метрика нужна. - Не молчать в Kubernetes ingress / Gateway-config. Если endpoint routed через отдельный path — уведомить infra, что он уйдёт; иначе Gateway продолжит роутить 404 вместо 410 после удаления handler’а.
- Не использовать
301 Moved Permanentlyкак замену deprecation. Редиректы ломают POST-запросы (клиент не переходит), кэшируются неправильно, не передают метрики использования.
Чеклист
Перед мержем deprecation PR (Фаза 1–2):
- Issue с label
deprecationсоздан, указаны replacement и sunset date. -
CHANGELOG.mdобновлён (## Deprecatedсекция). - Handler выставляет
Sunset,Deprecation,Linkheaders. - Метрика
deprecated_endpoint_calls_total{method, route}инкрементится на каждый вызов. - Alert
DeprecatedEndpointStillUsedдобавлен в alerts.yaml. - OpenAPI-спека помечена
deprecated: trueс пояснением. - Mobile/web-команды уведомлены (для public) / downstream- сервисы (для internal).
Перед мержем sunset PR (Фаза 4):
- 30+ дней прошло с последнего вызова endpoint’а (график
deprecated_endpoint_calls_totalв Grafana плоский). - Handler возвращает 410 Gone с пояснением и ссылкой на replacement.
- Announce в status-page / canal о финальном sunset’е.
Перед мержем removal PR (Фаза 5):
- 30+ дней 410 Gone без всплесков.
- Handler, route, тесты, OpenAPI-entry удалены.
- Метрика и alert удалены.
-
CHANGELOG.mdобновлён (## Removedсекция).
Связанные разделы
../conventions/api-versioning— когда bump’ать/v1/→/v2/vs когда достаточно deprecation одного endpoint’а.../conventions/http-api— формат ответа, status code’ы, правила для/v1/*vs/internal/*.../conventions/db-pgx#expand-contract— удаление колонки после удаления последнего читателя.add-migration— если deprecation сопровождается schema-изменениями.deploy-service/rollback-deploy— для выкатки / отката фаз.../services-catalog— обновить после удаления.