Learning Platform
Глоссарий Troubleshooting
Урок 03.03 · 17 мин
Начальный
HTTPMethodsIdempotencyRESTSafe methods

HTTP-методы — глаголы протокола

В HTTP метод — это глагол. URL — существительное (что: ресурс). Метод — глагол (что с этим ресурсом сделать). GET /api/users/42 — «получить пользователя 42». DELETE /api/users/42 — «удалить пользователя 42». Простая семантика, но за ней стоят два важных свойства, которые определяют поведение клиентов в production: safety и idempotency.

В этом уроке разберём все методы, поймём, чем они отличаются, и почему понимание идемпотентности — это разница между retry-логикой, которая работает, и retry-логикой, которая создаёт дубликаты.


Восемь методов, которые надо знать

HTTP/1.1 определяет 9 методов; OpenAPI 3.2 (черновик 2026 года) добавляет 10-й — QUERY. Junior DE сталкивается со всеми регулярно.

HTTP-методы и их назначение
GETПолучить ресурс. Самый частый метод. ~70-80% запросов в типичном API. Не должен менять состояние сервера
POSTСоздать ресурс ИЛИ выполнить действие, которое не вписывается в другие методы. Самый универсальный, но и самый размытый
PUTПолная замена ресурса. Шлёшь полный объект -- сервер заменяет старый этим целиком
PATCHЧастичное обновление. Шлёшь только изменённые поля. RFC 5789. Часто JSON Patch (RFC 6902) или JSON Merge Patch (RFC 7396)
DELETEУдалить ресурс. Обычно без тела. Возвращает 204 No Content или 200 с подтверждением
HEADКак GET, но без тела ответа. Удобно для проверки существования, размера, last-modified без скачивания тела
OPTIONSУзнать, какие методы поддерживает endpoint. Используется в CORS preflight (модуль 3)
QUERYНовый метод из OpenAPI 3.2 / draft RFC. GET с телом для сложных поисковых запросов. Сейчас редкий, но может стать стандартом

Кроме них существуют CONNECT (для прокси/туннелей) и TRACE (debug, обычно отключён в production по соображениям безопасности). Junior DE с ними почти не сталкивается.


GET: получить данные

GET — самый частый метод. Получить пользователя, список заказов, поисковую выдачу. Параметры передаются в URL (path или query string), не в теле.

curl 'https://api.github.com/users/torvalds'

curl 'https://api.github.com/search/repositories?q=python&per_page=5'

Ключевые свойства GET:

  • Safe — не меняет состояние сервера. Можно повторять сколько угодно — ничего не сломается.
  • Idempotent — следствие safety. Повторение даёт тот же результат.
  • Cacheable — браузеры, CDN, прокси кэшируют GET-ответы (если заголовки разрешают).
  • Тело обычно игнорируется. Технически RFC 9110 разрешает GET с телом, но большинство клиентов и серверов его игнорирует. Не делайте так.
WARNING

GET НЕ ДОЛЖЕН менять состояние. Никогда не делайте ‘GET /api/orders/42/delete’ — это нарушение HTTP. Поисковые роботы и кэши будут случайно удалять данные. Используйте DELETE.


POST: создать или выполнить действие

POST — самый универсальный метод. Стандартное использование:

  1. Создание нового ресурса. POST /api/users с телом — создаёт юзера, сервер генерирует ID.
  2. Действия, которые не вписываются в другие методы. Например, POST /api/payments/42/refund — действие refund на платёж 42.
  3. Сложные поисковые запросы. Когда параметры не помещаются в query string или должны быть структурированными — иногда используют POST вместо GET.
# Создать ресурс:
curl -X POST https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"[email protected]"}'

# Сервер вернёт что-то вроде:
# HTTP/1.1 201 Created
# Location: /api/users/42
# Content-Type: application/json
# {"id":42,"name":"Alice","email":"[email protected]"}

Свойства POST:

  • Не safe — меняет состояние сервера.
  • Не idempotent — повторный POST создаст ещё один ресурс. Это критично для retry-логики.
  • Не cacheable по умолчанию (можно сделать через явные заголовки, но обычно не делают).

PUT: полная замена ресурса

PUT означает: «вот тебе полный объект, замени старое значение по этому URL». Это идемпотентно: первый и сотый PUT с одинаковым телом дают одно и то же состояние сервера.

# Заменить пользователя 42 -- нужно прислать ВСЕ поля:
curl -X PUT https://api.example.com/users/42 \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"[email protected]","age":30}'

# Если повторить тот же запрос -- состояние сервера не изменится.
# В этом и есть идемпотентность.

Ключевая особенность PUT vs POST: клиент знает URL заранее. С POST сервер сам генерирует ID и говорит его в Location. С PUT клиент сам выбирает URL — это типично для случаев, когда ID — natural key (например, имя файла в S3).

# Загрузка файла в S3 -- PUT, потому что URL = ключ файла:
curl -X PUT https://my-bucket.s3.amazonaws.com/data/2026-05-14.csv \
  -H 'Authorization: AWS4-HMAC-SHA256 ...' \
  --data-binary @local-file.csv

Свойства PUT:

  • Не safe — меняет состояние.
  • Idempotent — повторение даёт то же состояние. Это и есть фундаментальное отличие от POST.

PATCH: частичное обновление

PATCH появился позже остальных (RFC 5789, 2010 год). Идея: отправить только изменения, не весь объект.

# Изменить только email пользователя 42:
curl -X PATCH https://api.example.com/users/42 \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]"}'

Тут возникает вопрос: «как именно применить изменения?» Стандарт PATCH сам по себе не определяет формат. На практике используют один из трёх форматов:

Три формата PATCH
JSON Merge PatchRFC 7396. Простой merge: поля в патче перезаписывают поля в объекте. null = удалить поле. application/merge-patch+json. Самый частый
JSON PatchRFC 6902. Массив операций: [{op:'replace', path:'/email', value:'[email protected]'}]. Поддерживает add/remove/replace/move/copy/test. application/json-patch+json
CustomAPI сам решает, как интерпретировать тело PATCH. Обычно как Merge Patch, но без явного Content-Type. Менее формально, более распространено на практике

Свойства PATCH:

  • Не safe.
  • Идемпотентность зависит от семантики. JSON Merge Patch — обычно идемпотентен (повторное {"email":"[email protected]"} даст то же состояние). JSON Patch с операцией add к массиву — НЕ идемпотентен (каждый патч добавит ещё элемент).

DELETE: удалить ресурс

DELETE — то, что написано: удалить. Обычно без тела запроса, ID в URL.

curl -X DELETE https://api.example.com/users/42
# Ответ: 204 No Content (без тела) или 200 OK с подтверждением

Свойства DELETE:

  • Не safe — удаляет данные.
  • Idempotent — это интересный момент. Первый DELETE удалил ресурс. Второй DELETE того же ресурса вернёт 404 Not Found, но состояние сервера то же самое (ресурс по-прежнему отсутствует). Отсюда и идемпотентность: важно состояние сервера, а не ответ клиенту.
NOTE

В классе ‘идемпотентен’ у DELETE есть нюанс: если на сервере есть soft-delete (помечает удалённым, не удаляет физически), и DELETE второй раз ничего не делает с уже удалённым — то идемпотентно. Если каждый DELETE инкрементирует счётчик ‘попыток удаления’ для аналитики — формально это уже не идемпотентно. На практике большинство API считают DELETE идемпотентным.


HEAD: GET без тела

HEAD — это «дай заголовки, но не давай тело». Сервер обрабатывает запрос как GET (генерирует ответ), но в TCP-канал отправляет только заголовки.

# Проверить, существует ли файл и узнать его размер:
curl -I https://example.com/big-file.zip
# HTTP/1.1 200 OK
# Content-Length: 524288000
# Content-Type: application/zip
# Last-Modified: Thu, 14 May 2026 10:00:00 GMT

# Это позволило узнать размер (500 MB) без скачивания самого файла

Применения:

  • Проверка существования (200 vs 404) без загрузки тела.
  • Получение метаданных (размер, тип, last-modified).
  • Проверка кэша через ETag — клиент сравнивает свой кэшированный ETag с серверным.

Свойства: safe и idempotent (как GET).


OPTIONS: метаданные endpoint

OPTIONS отвечает на вопрос «какие методы поддерживает этот endpoint?» Используется браузерами для CORS preflight (модуль 3).

curl -X OPTIONS https://api.example.com/users -i

# Пример ответа:
# HTTP/1.1 200 OK
# Allow: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE

Свойства: safe и idempotent.


QUERY: GET с телом (OpenAPI 3.2 draft)

В 2024-2026 годах в IETF идёт работа над методом QUERY: семантика как у GET (safe, idempotent, cacheable), но с телом запроса. Идея решает практическую проблему: иногда поисковый запрос настолько сложный (фильтры, сортировки, фасеты), что не помещается в query string или становится нечитаемым.

QUERY /api/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "filters": {
    "status": ["active", "pending"],
    "created_after": "2026-01-01"
  },
  "sort": [{"field": "created_at", "order": "desc"}],
  "limit": 100
}

На май 2026 QUERY поддерживается в Apollo Router, Cloudflare Workers и черновике OpenAPI 3.2. Большинство клиентов и серверов пока не знает про него. Junior DE редко встретит, но полезно знать о тренде.

Свойства: safe, idempotent, cacheable (по дизайну).


Сводная таблица: safety и idempotency

Свойства HTTP-методов
GETSafe + Idempotent + Cacheable. Самый 'безопасный' метод. Можно retry без последствий
HEADТо же, что GET, но без тела. Все свойства такие же
OPTIONSSafe + Idempotent, но обычно не кэшируется
PUTНЕ safe (меняет состояние), НО Idempotent. Можно retry -- будет то же состояние
DELETEНЕ safe (удаляет), НО Idempotent. После N удалений ресурс отсутствует так же, как после 1 удаления
QUERYSafe + Idempotent + Cacheable. Как GET, но с телом. Новый метод (2026)
POSTНЕ safe и НЕ idempotent. Каждый запрос потенциально создаёт новый ресурс. Retry опасен -- можно создать дубликаты
PATCHНЕ safe. Idempotency зависит от формата: Merge Patch обычно да, JSON Patch с add -- нет

Почему idempotency критична для retry-логики

Retries и rate limits: tenacity на практике

Junior пишет ETL-скрипт. Он создаёт пользователей в API партнёра:

import requests

for user in user_list:
    response = requests.post('https://api.partner.com/users', json=user)
    # ... иногда падает с 500. Что делать?

Junior, который не знает про идемпотентность, добавляет retry:

for user in user_list:
    for attempt in range(3):
        try:
            response = requests.post('https://api.partner.com/users', json=user)
            response.raise_for_status()
            break
        except requests.HTTPError:
            time.sleep(2 ** attempt)

И через неделю обнаруживает, что у партнёра куча дубликатов. Почему: POST не идемпотентен. Если первый запрос дошёл до сервера, сервер создал юзера, но ответ потерялся по сети — клиент получит timeout, попробует ещё раз, и сервер создаст ВТОРОГО юзера с теми же данными.

Решения:

  1. Использовать PUT с known ID, если API поддерживает: PUT /users/{external_id}. Тогда retry безопасен.
  2. Использовать Idempotency-Key, если API поддерживает (Stripe, GitHub, AWS): клиент генерирует уникальный UUID для каждой операции, сервер дедуплицирует по нему.
import uuid
import requests

for user in user_list:
    idempotency_key = str(uuid.uuid4())  # один UUID на одну операцию
    for attempt in range(3):
        try:
            response = requests.post(
                'https://api.partner.com/users',
                json=user,
                headers={'Idempotency-Key': idempotency_key}
            )
            response.raise_for_status()
            break
        except requests.HTTPError:
            time.sleep(2 ** attempt)

Подробно про retry — модуль 9. Здесь главное запомнить: POST + retry без Idempotency-Key = дубликаты. Это ловушка номер один в ETL.


Попробуй сам

Попробуйте все методы на httpbin.org:

# GET
curl https://httpbin.org/get

# HEAD -- заметьте, тела нет
curl -I https://httpbin.org/get

# POST
curl -X POST https://httpbin.org/post \
  -H 'Content-Type: application/json' \
  -d '{"name":"test"}'

# PUT
curl -X PUT https://httpbin.org/put \
  -H 'Content-Type: application/json' \
  -d '{"name":"test"}'

# PATCH
curl -X PATCH https://httpbin.org/patch \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]"}'

# DELETE
curl -X DELETE https://httpbin.org/delete

# OPTIONS -- посмотрите Allow в ответе
curl -X OPTIONS https://httpbin.org/get -i

Httpbin echo-возвращает то, что вы прислали. Посмотрите, как разные методы и тела влияют на ответ.


Проверка знанийKnowledge check
Junior пишет ETL: каждые 5 минут шлёт POST /api/orders с новым заказом из очереди. Иногда сервер возвращает timeout, и Junior добавляет retry. Через месяц партнёр жалуется на дубликаты. Что делать?
ОтветAnswer
Корень проблемы: POST не идемпотентен. Когда клиент получает timeout -- он не знает, что произошло на сервере. Возможны три сценария: (1) запрос не дошёл, сервер ничего не создал; (2) запрос дошёл, сервер создал ресурс, но ответ потерялся по сети; (3) запрос дошёл, сервер ещё обрабатывает. Retry в случае (2) создаст дубликат. Решения: (а) Использовать заголовок Idempotency-Key -- генерировать UUID на каждую операцию ПЕРЕД первой попыткой и слать ОДИН и тот же UUID на всех retry. Сервер дедуплицирует по этому ключу. Это поддерживают Stripe, GitHub, AWS, многие современные API. (б) Если API поддерживает PUT с external_id (например, PUT /api/orders/{client_order_id}), использовать PUT вместо POST -- он идемпотентен по дизайну. (в) В крайнем случае -- после timeout делать GET по какому-то natural key и проверять, создался ли ресурс. Это сложнее и медленнее. Главный урок: когда видите retry-логику с POST без Idempotency-Key -- это бомба замедленного действия. На больших объёмах рано или поздно будут дубликаты.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что значит, что метод 'идемпотентен'?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5