APIСтандартизация
тема 1
Открыть слайдами →

104Совместимость каждая компания определяет сама

Одно и то же изменение — новое поле в ответе — GitHub считает совместимым, а Azure нет.

Breaking changes are changes that can potentially break an integration.

Перевод: breaking changes — это изменения, которые потенциально могут сломать интеграцию.

GitHub Docs · API Versions ↗

В списке совместимых изменений GitHub есть «Adding a response field» — добавление поля в ответ.

Teams MAY define backwards compatibility as their business needs require. For example, Azure defines the addition of a new JSON field in a response to be not backwards compatible.

Перевод: команды MAY определять обратную совместимость так, как требует их бизнес. Например, Azure считает добавление нового поля JSON в ответ обратно несовместимым изменением.

Azure REST API Specs · Breaking changes guidelines ↗
github.com · Azure/azure-rest-api-specsСКРИНШОТ · 16.09.2026
Документ Azure о breaking changes: добавление нового поля в ответ названо несовместимым. Документ помечен как устаревший и ссылается на актуальную политику. Клик — увеличить.

Поэтому политика совместимости записывается явно. В учебном проекте принято правило GitHub, и клиенты обязаны игнорировать неизвестные поля: docs/VERSIONING.md. Список breaking changes GitHub разобран на слайде 69.

105Четыре места для версии API

Слайд 71 показал версию в пути. У Microsoft внутри одной компании два разных решения.

КтоГде версияПример
GitHub ↗заголовок, версия названа датойX-GitHub-Api-Version: 2026-03-10
Microsoft Graph ↗путь/v1.0/users, /beta/users
Azure ↗query-параметр?api-version=2024-05-01
Box ↗отдельный файл описания на версиюopenapi-v2025.0.json
Учебный проектпуть/api/v1/tickets
`version` can be v1.0 or beta.

Перевод: сегмент version может быть v1.0 или beta.

Microsoft Graph REST API Guidelines ↗
The api-version query parameter (?api-version=) is required for all requests

Перевод: query-параметр api-version обязателен во всех запросах.

Azure REST API Guidelines · текст ошибки, который требуется возвращать ↗
github.com · microsoft/api-guidelinesСКРИНШОТ · 16.09.2026
GuidelinesGraph.md, строки 134–138, подсвечены GitHub по ссылке на строки: структура URL и «`version` can be v1.0 or beta». Клик — увеличить.

106Deprecation и Sunset в каждом ответе

Пометку deprecated в OpenAPI увидит тот, кто читает документацию. Заголовок в ответе увидит программа, которая всё ещё вызывает устаревшую операцию.

During the deprecation phase, the producer should add a Deprecation: <timestamp> (see RFC 9745 section 2) and - if also planned - a Sunset: <date-time> (see RFC 8594 section 3) header on each response affected by a deprecated element

Перевод: на этапе устаревания владелец API должен добавлять заголовок Deprecation со временем (RFC 9745, раздел 2) и, если отключение запланировано, заголовок Sunset с датой (RFC 8594, раздел 3) в каждый ответ, который затрагивает устаревший элемент.

Zalando · правило 189 ↗
настоящий ответ учебного API
GET /api/v1/tickets/open?limit=1

HTTP/1.1 200
deprecation: @1789516800
sunset: Mon, 01 Mar 2027 00:00:00 GMT
ЗаголовокДокументСтатус
DeprecationRFC 9745 ↗Standards Track
SunsetRFC 8594 ↗Informational
Swagger UI · listOpenTicketsСКРИНШОТ · 16.09.2026
Устаревшая операция: путь зачёркнут, в ответе 200 описаны заголовки Deprecation и Sunset. Клик — увеличить.

107429: одинаковое имя заголовка — разный смысл

Код 429 и Retry-After определены в RFC. Заголовки X-RateLimit-* стандартом не являются, и компании понимают их по-разному.

The response representations SHOULD include details explaining the condition, and MAY include a Retry-After header indicating how long to wait before making a new request.

Перевод: ответ SHOULD объяснять причину и MAY содержать заголовок Retry-After, который показывает, сколько ждать перед новым запросом.

RFC 6585 · раздел 4 · Standards Track ↗
X-RateLimit-Reset: The relative time in seconds when the rate limit window will be reset. Beware that this is different to Github and Twitter’s usage of a header with the same name which is using UTC epoch seconds instead.

Перевод: X-RateLimit-Reset — относительное время в секундах до сброса окна лимита. Учтите, что GitHub и Twitter используют заголовок с тем же именем иначе: там время в секундах UTC epoch.

Zalando · правило 153 ↗

Стандарт в работе — draft-ietf-httpapi-ratelimit-headers ↗, активный черновик, версия 11. Он вводит заголовки RateLimit и RateLimit-Policy.

настоящий ответ учебного API
POST /api/v1/tickets   # третий запрос при лимите 2 в минуту

HTTP/1.1 429
retry-after: 60
x-ratelimit-limit: 2
x-ratelimit-remaining: 0
x-ratelimit-reset: 60
content-type: application/problem+json

{
  "type": ".../problems/http-429",
  "title": "Too many requests",
  "status": 429,
  "detail": "Write limit of 2 requests per minute exceeded"
}
Доказательство: как GitHub описывает x-ratelimit-reset
docs.github.com · Rate limitsСКРИНШОТ · 16.09.2026
GitHub Docs: x-ratelimit-reset — время сброса в секундах UTC epoch. Клик — увеличить.

108ETag и If-Match: не потерять чужую правку

Два оператора открыли одно обращение. Первый закрыл его, второй через минуту меняет описание по старой копии. Условный запрос не даёт второму затереть изменение первого.

The "If-Match" header field makes the request method conditional on the recipient origin server either having at least one current representation of the target resource, when the field value is "*", or having a current representation of the target resource that has an entity tag matching a member of the list of entity tags provided in the field value.

Перевод: заголовок If-Match делает метод запроса условным: сервер выполняет его, только если у ресурса есть текущее представление (при значении «*») или если entity tag текущего представления совпадает с одним из перечисленных в заголовке.

RFC 9110 · раздел 13.1.1 · Standards Track ↗
КтоСила правила
Zalando, правило 182 ↗MAY
Azure ↗DO — поддерживать If-Match и If-None-Match, возвращать ETag
GitHub ↗большинство endpoints отдают etag; 304 не расходует лимит
1. копия не изменилась — тело не передаётся
GET /api/v1/tickets/a8d04cb9-…
If-None-Match: "4fec2a976840743a"

HTTP/1.1 304
etag: "4fec2a976840743a"
2. обращение уже изменили — сервер отказывает
PATCH /api/v1/tickets/a8d04cb9-…
If-Match: "4fec2a976840743a"      # версия до чужой правки

HTTP/1.1 412
content-type: application/problem+json

{
  "title": "Precondition failed",
  "detail": "Ticket was changed after the version in If-Match was read"
}

109Idempotency-Key: распространённая практика без стандарта

POST не идемпотентен. Если ответ потерялся в сети и клиент повторил запрос, появится второе обращение. Ключ идемпотентности позволяет серверу узнать повтор.

datatracker.ietf.orgСКРИНШОТ · 16.09.2026
Черновик draft-ietf-httpapi-idempotency-key-header: статус Expired Internet-Draft. Клик — увеличить.
If there is an attempt to reuse an idempotency key with a different request payload, the resource SHOULD reply with a HTTP 422 status code with body containing a link pointing to relevant documentation.

Перевод: при попытке повторно использовать ключ с другим телом запроса ресурс SHOULD ответить кодом 422 с телом, в котором есть ссылка на документацию.

IETF draft-07 · срок действия истёк ↗
Idempotency keys are up to 255 characters long. … You can remove keys from the system automatically after they’re at least 24 hours old.

Перевод: ключ идемпотентности занимает до 255 символов… Ключи можно автоматически удалять, когда им не меньше 24 часов.

Stripe API · Idempotent requests ↗
настоящие ответы учебного API, сокращено
POST /api/v1/tickets  Idempotency-Key: 8e03978e-…  {"title": "Не работает VPN"}
HTTP/1.1 201   id: 6dfeb4e3-…

POST /api/v1/tickets  Idempotency-Key: 8e03978e-…  {"title": "Не работает VPN"}
HTTP/1.1 201   id: 6dfeb4e3-…   # тот же id, обращений по-прежнему 4

POST /api/v1/tickets  Idempotency-Key: 8e03978e-…  {"title": "Не работает почта"}
HTTP/1.1 422   "Idempotency-Key was already used with a different request body"

Длина ключа и срок хранения взяты из Stripe и Zalando, правило 230 ↗, код 422 — из черновика IETF.

110Идентификатор запроса: имя выбирает компания

Одна операция проходит через клиент, gateway и сервис. Общий идентификатор позволяет найти её записи во всех логах. Стандартного имени у этого заголовка нет.

КтоЗаголовок
Zalando, правило 233 ↗X-Flow-ID
Azure ↗x-ms-client-request-id
Учебный проектX-Request-Id
If the caller provides this header the service **must** include this in their log entries to facilitate correlation of log entries for a single request. Because this header can be client-generated, it should not …

Перевод: если вызывающая сторона передала этот заголовок, сервис must записывать его в логи, чтобы связать записи одного запроса. Поскольку значение может сформировать клиент, оно не должно… (дальше Azure ограничивает его использование).

Azure REST API Guidelines · correlation headers ↗
клиент передал свой идентификатор
GET /api/v1/tickets/a8d04cb9-…
X-Request-Id: support-case-1842

HTTP/1.1 200
x-request-id: support-case-1842
значение не прошло проверку формата (тест test_unsafe_request_id_is_replaced)
GET /api/v1/tickets
X-Request-Id: bad value; drop table

HTTP/1.1 200
x-request-id: 3c1f…            # UUID сервера
Доказательство: правило Zalando 233
opensource.zalando.com · rule 233СКРИНШОТ · 16.09.2026
Правило Zalando 233: MUST support X-Flow-ID. Клик — увеличить.

111CORS: заголовок пришёл, а скрипт его не видит

Браузерный клиент на другом домене получает от сервера ETag и Location, но JavaScript прочитает их, только если сервер явно разрешил.

`Access-Control-Expose-Headers` Indicates which headers can be exposed as part of the response by listing their names.

Перевод: Access-Control-Expose-Headers перечисляет имена заголовков, которые можно открыть скрипту как часть ответа.

Fetch Standard · CORS protocol ↗
app/main.py (фрагмент)
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,
    expose_headers=[
        "ETag", "Location", "X-Request-Id", "Retry-After",
        "Deprecation", "Sunset",
        "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset",
    ],
)
настоящий ответ учебного API
GET /api/v1/tickets/open
Origin: http://localhost:5173

HTTP/1.1 200
access-control-allow-origin: http://localhost:5173
access-control-expose-headers: ETag, Location, X-Request-Id, Retry-After, Deprecation, Sunset, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

Добавили в контракт новый заголовок ответа — добавьте его в expose_headers, иначе браузерный клиент его не получит.

112OAuth 2.0 описывают вместе с сервером авторизации

OpenAPI позволяет описать OAuth 2.0 со схемами authorizationCode и clientCredentials. Но схема в контракте — обещание клиенту, и обещать можно только то, что работает.

Что сравниваемУчебный стендProduction
Кто выдаёт токентокены заданы переменными окружениясервер авторизации по OAuth 2.0 / OpenID Connect
Схема в OpenAPIBearerAuth: type: http, scheme: bearertype: oauth2 с flows и scopes
Что проверяет сервиссовпадение строки токенаподпись, срок действия, scopes

OpenAPI 3.2.0, OAuth Flows Object ↗ описывает адреса авторизации и выдачи токена. Zalando, правило 104 ↗ требует защищать все endpoints.

Если описать в учебном контракте oauth2 без сервера авторизации, Swagger UI покажет кнопку входа, которая никуда не ведёт. Такое описание нарушает главное свойство контракта: он должен совпадать с поведением API.

Схему OAuth 2.0 добавляют в тот же pull request, что и подключение сервера авторизации.

113Что изменилось в учебном API 1.1.0

Все механизмы этой части добавлены в учебный проект как совместимое изменение: версия 1.0.0 стала 1.1.0.

CHANGELOG.md (сокращено)
## [1.1.0] - 2026-09-16

### Added

- Заголовок X-Request-Id в каждом ответе.
- ETag, 304 Not Modified на совпавший If-None-Match.
- If-Match в PATCH, ответ 412 при устаревшей версии.
- Idempotency-Key в POST /api/v1/tickets.
- Лимит операций записи: 429 с Retry-After, X-RateLimit-*.
- CORS с открытыми заголовками контракта.
- Поле type в Problem Details ведёт на страницы видов ошибок.

### Deprecated

- GET /api/v1/tickets/open — используйте GET /api/v1/tickets?status=open.

31 тест проходит, export_openapi.py --check подтверждает совпадение контракта с кодом, Spectral замечаний не выдаёт. Проверено 16 сентября 2026 года.

Swagger UI · updateTicketСКРИНШОТ · 16.09.2026
PATCH /api/v1/tickets/{ticket_id}: параметр If-Match в списке параметров операции. Клик — увеличить.

Кадр крупным планом