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 сталкивается со всеми регулярно.
Кроме них существуют 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 с телом, но большинство клиентов и серверов его игнорирует. Не делайте так.
GET НЕ ДОЛЖЕН менять состояние. Никогда не делайте ‘GET /api/orders/42/delete’ — это нарушение HTTP. Поисковые роботы и кэши будут случайно удалять данные. Используйте DELETE.
POST: создать или выполнить действие
POST — самый универсальный метод. Стандартное использование:
- Создание нового ресурса.
POST /api/usersс телом — создаёт юзера, сервер генерирует ID. - Действия, которые не вписываются в другие методы. Например,
POST /api/payments/42/refund— действие refund на платёж 42. - Сложные поисковые запросы. Когда параметры не помещаются в 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:
- Не 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, но состояние сервера то же самое (ресурс по-прежнему отсутствует). Отсюда и идемпотентность: важно состояние сервера, а не ответ клиенту.
В классе ‘идемпотентен’ у 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
Почему 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, попробует ещё раз, и сервер создаст ВТОРОГО юзера с теми же данными.
Решения:
- Использовать PUT с known ID, если API поддерживает:
PUT /users/{external_id}. Тогда retry безопасен. - Использовать 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-возвращает то, что вы прислали. Посмотрите, как разные методы и тела влияют на ответ.