Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 22 мин
Начальный
HTTPHeadersStatus CodesMethodscurl

Анатомия HTTP-запроса — что внутри request и response

Когда вы открываете сайт или дергаете API — ваш клиент шлёт HTTP-сообщение, сервер шлёт другое HTTP-сообщение в ответ. Это всё, что есть. Никакой магии. Сообщение — это просто текст, лежащий поверх TCP-соединения. Если вы научитесь читать этот текст — вы сможете дебажить почти любую сетевую проблему уровня приложения.

В этом уроке разбираем HTTP-сообщение по байтам. Что такое request-line, какие бывают методы, как устроены заголовки, что такое body, что значат статус-коды. Все примеры — реальный curl -v вывод, который вы можете повторить на своей машине прямо сейчас.

В курсе rest-api-fundamentals HTTP разбирается с точки зрения API-методологии — как проектировать ресурсы, как делать REST правильно. Здесь же мы смотрим на HTTP как на сетевой протокол: какие байты летят по проводам, как сервер парсит входящее, какой формат на проводе. Эти знания фундаментальны для понимания TLS, HTTP/2, прокси, кэширования — всего, что строится поверх HTTP.


Структура HTTP-сообщения

Любое HTTP-сообщение (и запрос, и ответ) имеет одну и ту же общую структуру:

<start-line>
<header1>: <value1>
<header2>: <value2>
...
<headerN>: <valueN>

<optional body>

Где start-line — это либо request-line (для запросов), либо status-line (для ответов). Дальше идут заголовки, по одному на строку, в формате Имя: Значение. Потом пустая строка (CRLF — два символа \r\n). Потом опциональное тело сообщения.

HTTP-сообщение -- три части
Start lineДля запроса -- request-line: METHOD path HTTP/version. Для ответа -- status-line: HTTP/version code reason. Одна строка, заканчивается CRLF
HeadersKey-value пары через двоеточие. Каждый заголовок на своей строке. Имена case-insensitive (Content-Type == content-type)
CRLFПустая строка -- сигнал конца заголовков. После неё начинается тело (или сообщение заканчивается, если тела нет)
BodyОпциональное тело -- может быть JSON, HTML, бинарные данные, multipart-форма. Длина указывается в Content-Length или приходит chunked

Самое важное — понять, что между заголовками и телом всегда идёт пустая строка. Это маркер конца секции заголовков. Если её забыть — сервер будет ждать ещё заголовков и не начнёт парсить тело. Этот баг в самописных HTTP-клиентах — классика.


Request line: метод, путь, версия

Первая строка запроса выглядит так:

GET /api/users/torvalds HTTP/1.1

Здесь три части, разделённые пробелами:

  1. МетодGET. Какое действие хотим выполнить.
  2. Target/api/users/torvalds. Путь к ресурсу на сервере. Не URL целиком — именно path + query string.
  3. Версия протоколаHTTP/1.1.

Заметьте: домен (api.github.com) в request-line не передаётся. Сервер узнаёт его из заголовка Host: — именно так один сервер обслуживает множество сайтов на одном IP.

HTTP-методы

В HTTP стандартизировано девять методов. На практике используются восемь:

HTTP-методы и их семантика
GETПолучить ресурс. Безопасный (не меняет state), идемпотентный (повторный вызов даёт тот же результат). Не должен иметь body
HEADКак GET, но сервер возвращает только заголовки, без body. Используется для проверки существования, размера, ETag
OPTIONSУзнать, какие методы поддерживает endpoint. Также используется браузером для CORS preflight-запросов
POSTСоздать ресурс или выполнить действие. Не идемпотентный -- повторный POST создаст ещё одну запись. Может иметь body любого формата
PUTСоздать или полностью заменить ресурс по известному URI. Идемпотентный -- PUT /users/42 дважды оставит ровно одного user 42
PATCHЧастичное обновление ресурса. Не обязательно идемпотентный. Тело -- diff или JSON Patch (RFC 6902)
DELETEУдалить ресурс. Идемпотентный -- удалить уже удалённого = тот же результат. Тело обычно пустое
CONNECTОткрыть туннель через прокси (используется для HTTPS через HTTP-proxy). На практике редко руками
TRACEEcho-запрос: сервер вернёт то, что получил. Опасный с точки зрения безопасности, обычно отключен

Понятия безопасный и идемпотентный — ключевые в HTTP, потому что они определяют, что прокси и клиенты вправе делать с запросом:

  • Safe методы (GET, HEAD, OPTIONS) не меняют состояние сервера. Их можно кэшировать, повторять автоматически, выполнять параллельно.
  • Idempotent методы (GET, HEAD, PUT, DELETE, OPTIONS) при повторе дают тот же результат. Это значит, что клиент может безопасно ретраить запрос, если получил timeout.

POST — ни safe, ни idempotent. Если вы получили timeout на POST — вы не знаете, дошёл запрос или нет. Поэтому в проде POST часто требует idempotency key в заголовке (Idempotency-Key: <uuid>), чтобы сервер мог отличить повтор от нового запроса.


Status line: версия, код, reason

Первая строка ответа:

HTTP/1.1 200 OK

Тоже три части:

  1. ВерсияHTTP/1.1.
  2. Status code200. Трёхзначное число.
  3. Reason phraseOK. Текстовое описание для людей. Не используется машинами и в HTTP/2 не передаётся вообще.

Status codes по категориям

Первая цифра кода говорит о категории:

HTTP status codes по группам
1xxInformational. Промежуточные ответы. 100 Continue, 101 Switching Protocols (для WebSocket upgrade), 103 Early Hints
2xxSuccess. 200 OK -- стандартный успех. 201 Created -- ресурс создан. 204 No Content -- успех без тела. 206 Partial Content -- range request
3xxRedirection. 301 Permanent, 302/307 Temporary, 304 Not Modified (cache hit). Клиент должен сходить по новому URL из Location
4xxClient error. 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
5xxServer error. 500 Internal Server Error, 502 Bad Gateway (upstream not available), 503 Service Unavailable, 504 Gateway Timeout

На практике вам нужно знать наизусть примерно двадцать кодов. Запомните разницу между 4xx и 5xx: 4xx — виноват клиент (плохой запрос, нет прав, не найдено). 5xx — виноват сервер (упал, недоступен бэкенд, таймаут). От этого зависит, кому идти жаловаться.

Особенно важные коды для дебага:

  • 401 vs 403. 401 — я не знаю, кто вы (нет/плохой токен). 403 — я знаю, кто вы, но вам сюда нельзя. Если получаете 403 — проверяйте права доступа в системе, не токен.
  • 404 vs 410. 404 — ресурса не существует или вы не знаете. 410 — ресурс существовал, но удалён навсегда. На практике почти все ставят 404 и для удалённых.
  • 502 vs 503. 502 — nginx/балансировщик не смог достучаться до бэкенда. 503 — бэкенд жив, но сам сказал «я перегружен». В первом случае проблема в инфраструктуре, во втором — в backpressure.
  • 429 Too Many Requests. Rate limiting. Ответ обычно содержит Retry-After: <seconds>. Уважайте этот заголовок — не ретраите тут же.

Headers: метаданные сообщения

Заголовки — это пары Имя: Значение, по одному на строку. Имена case-insensitive (Content-Type == content-type), но в HTTP/1.1 принято писать в CamelCase. В HTTP/2 заголовки передаются в lowercase.

Каноническое имя заголовка может содержать буквы, цифры и дефис. Значение — любой текст без CR/LF. Если нужно перенос строки — используют , в Set-Cookie и других multi-value заголовках.

WARNING

В сыром HTTP заголовок не может содержать символ перевода строки внутри значения. Если ваш самописный код пишет значение через f"X-User: {name}", и name пришёл от пользователя — атакующий может вставить \r\n и инжектировать произвольные заголовки. Это называется HTTP Response Splitting и было реальной CVE-уязвимостью у многих приложений. Используйте библиотечный API для установки заголовков, а не строковые шаблоны.

Самые важные заголовки запроса

GET /api/users HTTP/1.1
Host: api.github.com
User-Agent: curl/8.4.0
Accept: application/json
Accept-Encoding: gzip, deflate
Accept-Language: ru-RU,en-US;q=0.5
Authorization: Bearer ghp_xxxxxxxxxxxxxx
Cookie: session_id=abc123; theme=dark
Content-Type: application/json
Content-Length: 87
Connection: keep-alive
If-None-Match: "etag-value-here"

Что значит каждый:

  • Host — обязательный с HTTP/1.1, иначе сервер вернёт 400. Содержит имя домена. Нужен для virtual hosting.
  • User-Agent — идентификатор клиента. Часто игнорируется, но некоторые сервера им filterят.
  • Accept — какие MIME-типы клиент готов принять в ответе. Без него сервер обычно отдаёт default.
  • Accept-Encoding — какие компрессии клиент понимает. Сервер ответит gzip/br/zstd, если знает.
  • Authorization — credentials. Bearer-токен, Basic auth (base64 user:pass), Digest и т.д.
  • Cookie — значения cookie, которые сервер ранее установил через Set-Cookie.
  • Content-Type — MIME-тип тела запроса. application/json, multipart/form-data, text/plain и т.д.
  • Content-Length — размер тела в байтах.
  • Connection — управление соединением. keep-alive — не закрывать после ответа. close — закрыть.
  • If-None-Match / If-Modified-Since — conditional GET для кэширования (отдельный урок).

Самые важные заголовки ответа

HTTP/1.1 200 OK
Date: Mon, 18 May 2026 14:23:01 GMT
Server: nginx/1.25.3
Content-Type: application/json; charset=utf-8
Content-Length: 1428
Content-Encoding: gzip
Cache-Control: public, max-age=3600
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Sun, 12 May 2026 18:00:00 GMT
Vary: Accept-Encoding
Set-Cookie: session_id=xyz; HttpOnly; Secure; SameSite=Lax
Strict-Transport-Security: max-age=31536000
  • Date — когда сервер сгенерировал ответ.
  • Server — название серверного софта. Часто скрывают по соображениям безопасности.
  • Content-Type — тип тела ответа.
  • Content-Encoding — сжатие тела. Если gzip, клиент должен распаковать.
  • Cache-Control — правила кэширования.
  • ETag / Last-Modified — маркеры версии для conditional GET.
  • Vary — по каким request-заголовкам меняется ответ. Важно для прокси и CDN.
  • Set-Cookie — сервер просит сохранить cookie. На каждом следующем запросе клиент пришлёт его обратно.
  • Strict-Transport-Security (HSTS) — говорит браузеру всегда использовать HTTPS для этого домена.

Body: тело сообщения

Тело — это произвольные байты. Никаких структурных правил у HTTP к телу нет; формат определяет Content-Type. JSON, HTML, XML, бинарный PNG, gzip-compressed stream — всё может быть телом.

Длина тела передаётся одним из двух способов:

  1. Content-Length: N — ровно N байт следом за заголовками. Сервер знает заранее, сколько байт читать.
  2. Transfer-Encoding: chunked — неизвестная заранее длина. Тело идёт чанками: размер чанка в hex + CRLF + данные + CRLF + … + 0\r\n\r\n в конце.

Chunked-encoding нужен, когда сервер не знает заранее размер ответа — например, стримит сгенерированный JSON по мере чтения из БД, или это server-sent events.

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain

7\r\n
Hello, \r\n
6\r\n
World!\r\n
0\r\n
\r\n

В коде на Python вы chunked парсите вручную крайне редко — библиотеки делают это за вас. Но знать, что бывает, обязательно.


Реальный обмен — читаем curl -v

Самое полезное упражнение — запустить curl -v и прочитать всё построчно.

curl -v https://httpbin.org/anything/hello

Что вы увидите (отбрасываю TLS-handshake, на нём отдельный модуль):

> GET /anything/hello HTTP/1.1
> Host: httpbin.org
> User-Agent: curl/8.4.0
> Accept: */*
>

< HTTP/1.1 200 OK
< Date: Mon, 18 May 2026 14:23:01 GMT
< Content-Type: application/json
< Content-Length: 318
< Connection: keep-alive
< Server: gunicorn/19.9.0
< Access-Control-Allow-Origin: *
<
{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.4.0"
  },
  "method": "GET",
  "url": "https://httpbin.org/anything/hello"
}

Префикс > — то, что мы отправили на сервер. Префикс < — то, что вернулось. Пустые строки после заголовков (с одним > или <) — это CRLF, маркер конца заголовков.

httpbin.org удобен тем, что эхает обратно полный набор: что было в запросе, какие заголовки сервер увидел. Это идеальный полигон для экспериментов.


POST с телом

Простой POST с JSON:

curl -v -X POST https://httpbin.org/post \
  -H "Content-Type: application/json" \
  -d '{"name": "Linus", "project": "linux"}'

Что улетит:

POST /post HTTP/1.1
Host: httpbin.org
User-Agent: curl/8.4.0
Accept: */*
Content-Type: application/json
Content-Length: 36

{"name": "Linus", "project": "linux"}

Заметьте: Content-Length: 36 — curl сам посчитал длину тела. Сервер прочитает первые 36 байт после CRLF — это тело запроса.

Если вы пишете самописный HTTP-клиент (например, на raw socket), забыть Content-Length — очень распространённая ошибка. Сервер будет ждать тело и в итоге отвалится по timeout, или прочтёт мусор.


Попробуй сам

Откройте терминал и потренируйтесь видеть HTTP своими глазами:

# 1. Простейший GET -- посмотреть request и response целиком
curl -v https://httpbin.org/get 2>&1 | grep -E '^[<>]'

# 2. Разные методы -- посмотрите, как меняется request-line
curl -v -X DELETE https://httpbin.org/delete 2>&1 | grep -E '^[<>]'
curl -v -X PUT https://httpbin.org/put 2>&1 | grep -E '^[<>]'

# 3. Status codes -- запросите разные коды через httpbin
curl -v https://httpbin.org/status/200 2>&1 | grep -E '^[<>]'
curl -v https://httpbin.org/status/404 2>&1 | grep -E '^[<>]'
curl -v https://httpbin.org/status/500 2>&1 | grep -E '^[<>]'
curl -v https://httpbin.org/status/418 2>&1 | grep -E '^[<>]'  # I am a teapot

# 4. POST с разными форматами
curl -v -X POST -H "Content-Type: application/json" \
  -d '{"hello": "world"}' https://httpbin.org/post 2>&1 | tail -30

# 5. С кастомными заголовками
curl -v -H "X-My-Header: experiment" \
     -H "X-Trace-Id: abc-123" \
     https://httpbin.org/headers 2>&1 | tail -20

# 6. HEAD -- только заголовки, без тела
curl -v -I https://httpbin.org/get 2>&1 | grep -E '^[<>]'

# 7. Посмотреть, во что превратится команда -- готовый текст запроса
curl --trace-ascii /dev/stdout https://httpbin.org/get 2>&1 | head -40

Поэкспериментируйте: меняйте методы, добавляйте заголовки, смотрите, что отвечает сервер. Это лучший способ закрепить интуицию.


Что вы должны вынести из этого урока

  1. HTTP-сообщение — это текст: start-line, заголовки, пустая строка, опционально тело.
  2. Метод определяет действие; status-code — результат.
  3. Безопасность (safe) и идемпотентность — ключевые свойства методов, определяющие, что можно делать прокси и retry-логике.
  4. Заголовок Host обязателен в HTTP/1.1.
  5. Длина тела задаётся через Content-Length или Transfer-Encoding: chunked.
  6. curl -v показывает реальный обмен. Префикс > — запрос, < — ответ.

В следующем уроке разберём, как HTTP, будучи stateless, поддерживает состояние через cookies и sessions, и почему CORS вообще существует.

HTTP-методы и статус-коды с точки зрения API-дизайна HTTP как протокол — request/response lifecycle
Проверка знанийKnowledge check
Вы видите в логах балансировщика запрос: POST /api/orders HTTP/1.1, ответ 504 Gateway Timeout. Через 30 секунд клиент повторяет тот же POST, получает 200 OK с order_id. Через час в БД обнаруживается дубликат заказа на ту же сумму. Объясните, что произошло, и как такое предотвратить архитектурно.
ОтветAnswer
Произошёл классический сценарий "дублированный POST из-за retry на timeout". Цепочка: клиент послал POST, балансировщик передал его upstream-серверу. Upstream обработал запрос (создал заказ в БД), но ответ либо не успел вернуться к балансировщику, либо балансировщик не успел вернуть его клиенту -- сработал gateway timeout (504). Клиент, видя 504, решил повторить -- и второй POST создал второй заказ. POST не идемпотентен по своей семантике -- сервер не может отличить retry от нового запроса. Архитектурное решение: idempotency keys. Клиент при каждом "уникальном бизнес-действии" генерирует UUID и шлёт в заголовке Idempotency-Key. Сервер при получении смотрит -- если такой key уже видел, возвращает сохранённый прошлый ответ вместо повторного создания заказа. Если не видел -- обрабатывает и сохраняет (key, response) на некоторое время (обычно 24h). Stripe, PayPal, любые серьёзные платёжные API это делают. Альтернатива -- использовать PUT для создания с known ID на стороне клиента (PUT /orders/<client-uuid>), тогда вторая попытка PUT с тем же UUID просто перепишет существующий заказ -- идемпотентность встроенная.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. В HTTP-сообщении между секцией заголовков и телом идёт обязательная пустая строка (CRLF). Какой логический смысл этого разделителя для парсера сервера?

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

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

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

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