Модуль 2 · Правила HTTP

Справочник «Стандартизация HTTP API» · модуль 2 · правила HTTP

Глава 2.2

2.2

Методы
и их свойства

Метод указывает, какое действие выполняется над ресурсом. Смысл каждого метода задан стандартом, и на него опираются браузеры, прокси и клиентские библиотеки, поэтому выбирать метод произвольно нельзя. В этой главе разобраны пять методов учебного сервиса и два их свойства: safe и idempotent.

Категория
Правила HTTP
Уровень
Базовый
Связанные главы
1.3 Ресурсы · 2.3 Коды ответов · 4.3 Заголовки контракта
Зачем это знать
Чтобы повтор запроса после обрыва сети не создавал второе обращение

§1Метод — глагол контракта

Значение методов задаёт RFC 9110 — стандарт семантики HTTP. На описанный в нём смысл опираются кэши, прокси, браузеры и клиентские библиотеки. Если сервис использует метод вопреки стандарту, эти посредники начинают вести себя неправильно.

Раздел 9 RFC 9110: обзор методов и таблица со стандартными методами GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS
RFC 9110, раздел 9 «Methods». Таблица перечисляет стандартные методы и ссылается на подраздел с описанием каждого. Именно отсюда берутся определения, которыми мы пользуемся дальше.

В учебном сервисе используются пять методов. Ниже — каждый из них с настоящими ответами: команды выполнены на запущенном Service Desk API, вывод не переписан.

§2GET: чтение

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

чтение коллекции и элемента
GET /api/v1/tickets?status=open&limit=20
GET /api/v1/tickets/{ticket_id}

Возможных исходов у чтения немного: обращение найдено — 200 и тело; не найдено — 404 и описание ошибки; параметры не прошли проверку — 422.

Чего не делает GET

Не принимает тело, не меняет данные, не «отмечает просмотренным». Счётчик просмотров, который увеличивается при GET, — известный источник странных цифр: их накручивают поисковые роботы и предзагрузка браузера.

§3POST: создание

POST отправляет данные в коллекцию, и сервер создаёт новый ресурс. Главное свойство: POST не идемпотентен. Два одинаковых запроса создают два разных обращения. Так метод определён в стандарте: каждый POST на коллекцию — отдельное создание.

два одинаковых запроса подряд
POST /api/v1/tickets   {"title": "Не работает VPN"}
POST /api/v1/tickets   {"title": "Не работает VPN"}
ответы учебного сервиса
HTTP/1.1 201
location: /api/v1/tickets/d22cf098-4373-4bca-a643-5cca0a640a8a
{"id": "d22cf098-…", "title": "Не работает VPN", "status": "open"}

HTTP/1.1 201
location: /api/v1/tickets/b228d2d7-06ca-4eb9-8a72-386595ee39a9
{"id": "b228d2d7-…", "title": "Не работает VPN", "status": "open"}

# идентификаторы разные, обращений в коллекции стало на два больше

Отсюда практическая проблема. Клиент отправил запрос, сеть оборвалась, ответ не дошёл. Клиент не знает, создалось обращение или нет. Повторит — рискует получить дубль; не повторит — рискует потерять заявку.

Решение стандартом не описано, но практика сложилась: клиент присылает заголовок Idempotency-Key, а сервер запоминает ключ вместе с результатом. Повтор с тем же ключом возвращает первый результат и ничего не создаёт. Так делают Stripe и Zalando, так сделано в учебном проекте — подробный разбор в главе 4.3.

§4PUT и PATCH: замена и правка

Оба метода изменяют существующий ресурс, но по-разному. PUT передаёт полное новое представление: то, чего в теле нет, должно исчезнуть. PATCH передаёт только изменяемые поля, остальные остаются как были.

PUTЗамена целиком
  • тело содержит все поля ресурса;
  • пропущенное поле означает «его больше нет»;
  • идемпотентен: повтор даёт то же состояние.
PATCHЧастичное изменение
  • тело содержит только изменяемые поля;
  • остальное сервер не трогает;
  • идемпотентность зависит от смысла правки.

В учебном сервисе выбран PATCH: чаще всего меняется один статус, и заставлять клиента присылать заголовок с описанием ради этого не нужно. Посмотрим, что происходит при повторе.

две одинаковые правки подряд
PATCH /api/v1/tickets/84bb7367-…   {"status": "closed"}
PATCH /api/v1/tickets/84bb7367-…   {"status": "closed"}
ответы учебного сервиса
HTTP/1.1 200
etag: "1497f09b08f7a9b5"
{"id": "84bb7367-…", "status": "closed"}

HTTP/1.1 200
etag: "257551ee94925a8c"
{"id": "84bb7367-…", "status": "closed"}

Обратите внимание: статус после второй правки тот же, а etag изменился. Причина в поле updated_at — сервер обновил время изменения. Состояние ресурса по смыслу прежнее, представление — другое.

Идемпотентность говорит о состоянии ресурса после повтора, а не о совпадении байтов ответа. Второй PATCH не «закрыл обращение ещё раз» — оно уже было закрыто.

§5DELETE: удаление

DELETE удаляет ресурс. Успешный ответ не содержит тела: возвращать больше нечего, поэтому код 204 No Content.

два одинаковых удаления подряд
DELETE /api/v1/tickets/84bb7367-…
DELETE /api/v1/tickets/84bb7367-…
ответы учебного сервиса
HTTP/1.1 204

HTTP/1.1 404
content-type: application/problem+json
{
  "type": "…/problems/http-404",
  "title": "Resource not found",
  "status": 404,
  "detail": "Ticket 84bb7367-… does not exist"
}

Коды разные — 204 и 404, — но DELETE остаётся идемпотентным: после первого и после второго запроса состояние сервера одинаково, обращения нет. На этом примере видно, почему идемпотентность определяют через состояние сервера, а не через совпадение ответов.

§6Safe и idempotent

Эти два свойства часто смешивают. Различить их помогают два разных вопроса: «что делает запрос?» и «что будет, если выполнить его дважды?».

safeКлиент не просит изменить состояние. Такой запрос можно повторить, предзагрузить и закэшировать без последствий.GET, HEAD, OPTIONS
idempotentПовтор идентичного запроса оставляет состояние таким же, как после первого. Коды ответов при этом могут различаться.GET, PUT, DELETE
МетодSafeIdempotentЧто показывает учебный сервис
GETдадаповтор возвращает то же обращение, данные не меняются
POSTнетнетдва запроса — два обращения с разными id
PUTнетдаповтор замены даёт то же состояние ресурса
PATCHнетзависит от правки{"status": "closed"} — да; «увеличить счётчик на 1» — нет
DELETEнетда204, затем 404; обращения нет в обоих случаях

На эти свойства опираются механизмы повторов: клиентские библиотеки и балансировщики автоматически повторяют идемпотентные запросы при обрыве соединения и не повторяют POST.

§7Как выбрать метод

Метод выбирают по смыслу операции, а не по удобству клиента. Таблица ниже покрывает почти все случаи, которые встречаются в учебных и рабочих API.

Что нужно сделатьМетод и адресУспешный ответ
Получить список с фильтромGET /tickets?status=open200 + страница
Получить один объектGET /tickets/{id}200 + ETag
Создать объектPOST /tickets201 + Location
Изменить часть полейPATCH /tickets/{id}200 + новое представление
Заменить объект целикомPUT /tickets/{id}200 или 204
Удалить объектDELETE /tickets/{id}204 без тела
Выполнить действие («закрыть», «отправить»)PATCH статуса или POST на подресурспо смыслу действия

Последняя строка — та, где чаще всего появляются адреса с глаголами. Если действие меняет одно поле, это PATCH: «закрыть» — это {"status": "closed"}. Если действие создаёт новую сущность — например, отправку уведомления, — у неё есть своё имя, и тогда уместен POST /tickets/{id}/notifications.

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

«Идемпотентный — значит одинаковый ответ»Нет: речь о состоянии сервера. Повторный DELETE отвечает 404, но остаётся идемпотентным.
POST для чтения«Сложный фильтр не помещается в query» — частое объяснение. Ответ на POST не кэшируется и не повторяется посредниками; прежде чем так делать, стоит упростить фильтр.
GET, который меняет данныеСчётчики, отметки «прочитано», подтверждения по ссылке из письма. Любой предзагрузчик испортит такие данные.
PUT вместо PATCHЕсли клиент присылает не все поля, а сервер трактует это как PUT, недостающие поля обнуляются. Такую потерю данных замечают не сразу.

§9Проверьте себя

  1. Выполните два одинаковых POST и сравните id в ответах. Сколько обращений стало в коллекции?
  2. Удалите обращение дважды. Какие коды вернул сервер и почему метод всё равно идемпотентен?
  3. Почему у двух одинаковых PATCH получились разные etag?
  4. Клиент потерял ответ на POST из-за обрыва сети. Что ему делать и что должен предложить контракт?
  5. Операция «закрыть обращение» — какой метод и какой адрес? Обоснуйте выбор.
Свойстваsafe: GET, HEAD, OPTIONSidempotent: GET, PUT, DELETEPOST — ни то, ни другое
Коды успехаGET → 200POST → 201 + LocationPATCH → 200DELETE → 204
Повтор POSTIdempotency-Keyтот же ключ → тот же результат
Проверка смыслачто прошу сделать?что будет, если дважды?

Справочник «Стандартизация HTTP API» · модуль 2 · глава 2.2Макаров Максим Николаевич

Кадр