Skip to Content
How-toDeprecate и удалить endpoint

Как deprecate’нуть и удалить endpoint

Пошаговая процедура корректного изъятия HTTP-endpoint’а из публичного API без поломки mobile/web-клиентов, которые ещё его используют. Actual reference по формату URL, middleware, error- mapping — ../conventions/http-api. Версионирование API — ../conventions/api-versioning. Эта страница — процедура, от «решили выпилить» до «code deleted».

Содержание

Когда deprecate’нуть

Причины удалить endpoint:

  • Endpoint дублирует более новый (/v1/places/{id}/reviews vs /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

Действия:

  1. Issue в сервис-репо с тегом deprecation и следующей информацией:

    • Endpoint (метод + путь + версия): GET /v1/reviews/by-place/{id}.
    • Replacement (если есть): GET /v1/reviews?place_id={id}.
    • Sunset date: конкретная дата, не «через 3 месяца».
    • Причина: одна строка.
  2. Changelog entry в сервис-репо CHANGELOG.md:

    ## [Unreleased] ### Deprecated - `GET /v1/reviews/by-place/{id}` — use `GET /v1/reviews?place_id={id}` instead. Sunset: 2026-07-20.
  3. Уведомление клиентов. Для public API — запись в status-page / release-notes, которую читает mobile/web-команда. Для /internal/* — сообщение в backend on-call канал с owner’ом downstream-сервиса.

  4. Обновление ../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 с нулевым использованием:

  1. PR: удалить handler, route, OpenAPI-спецификацию, тесты.

  2. Удалить метрику и alert (DeprecatedEndpointCalls, DeprecatedEndpointStillUsed).

  3. 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}`.
  4. 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 в коде».

  1. Поднять новый endpoint (GET /v1/reviews?place_id=X) с полностью повторяющей функциональностью. Обязательно: те же статусы, формат ответа, права доступа, validation rules. Тест integration’ом оба одновременно на одинаковых fixtures.
  2. Announce deprecation старого (см. Фаза 1).
  3. Пройти Фазы 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, Link headers.
  • Метрика 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 секция).

Связанные разделы

Last updated on