Learning Platform
Глоссарий Troubleshooting
Урок 03.02 · 18 мин
Начальный
HTTPRequestResponseHeaderscurl

Анатомия запроса и ответа

В прошлом уроке мы установили, что HTTP/1.1 — текстовый протокол поверх TCP. В этом уроке полезем в детали: что именно лежит в HTTP-сообщении, какая у него структура, и как увидеть это руками. К концу урока вы сможете прочитать любой HTTP-обмен, как программист читает код, а не как пользователь смотрит на иероглифы.


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

И запрос, и ответ имеют одинаковую общую структуру:

Структура HTTP-сообщения
Start-lineПервая строка. Для запроса -- request-line (METHOD URL VERSION). Для ответа -- status-line (VERSION CODE PHRASE)
HeadersСписок 'Имя: Значение' по одному на строку. Каждая строка кончается CRLF (\r\n). Регистронезависимы
Empty lineПустая строка (CRLF). Это разделитель между заголовками и телом. БЕЗ ПУСТОЙ СТРОКИ парсер не поймёт, где кончились заголовки
BodyОпциональное тело. Может быть JSON, HTML, бинарка, что угодно. Размер указан в Content-Length или передаётся chunked

Все строки в HTTP/1.1 разделены \r\n (CR LF, два байта: 0x0D 0x0A). Это исторический выбор из эпохи telnet. На вашем компьютере при выводе на экран \r\n отображается как обычный перенос строки, и вы не видите разницы между \n и \r\n — но протокол строгий: только \r\n.


Анатомия HTTP-запроса

Разберём вот такой запрос подробно:

POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 28
Authorization: Bearer abc123
User-Agent: MyClient/1.0

{"name":"Alice","age":30}
Запрос построчно
POSTHTTP-метод. Что мы хотим сделать с ресурсом. POST = создать или обработать
/api/usersПуть (path) к ресурсу. Часть URL после хоста. Может содержать query string: /api/users?role=admin
HTTP/1.1Версия протокола. Сервер отвечает обычно той же версией, но может предложить downgrade
HostИмя хоста, к которому идёт запрос. ОБЯЗАТЕЛЬНЫЙ заголовок в HTTP/1.1 (один сервер может обслуживать несколько доменов на одном IP)
Content-TypeКакой формат тела. application/json -- это JSON. application/x-www-form-urlencoded -- формы. multipart/form-data -- формы с файлами
Content-LengthРазмер тела в байтах. Обязателен, если тело есть и не используется chunked encoding
AuthorizationТокен или другие credentials. Bearer abc123 -- самый частый формат для API
User-AgentИдентификация клиента: тип, версия. Многие API логируют это для метрик и могут блокировать без User-Agent
ТелоJSON-данные. После пустой строки. Размер должен совпадать с Content-Length, иначе сервер закроет соединение

Несколько важных моментов:

  1. Имена заголовков case-insensitive. Content-Type, content-type, CONTENT-TYPE — это один заголовок. По конвенции пишут в формате Pascal-Case-Через-Дефис.
  2. Значения заголовков case-sensitive (обычно). Bearer abc123bearer abc123 для большинства серверов.
  3. Один заголовок может встретиться несколько раз. Например, Set-Cookie часто несколько штук в ответе, или Accept-Encoding: gzip и Accept-Encoding: deflate — стандарт разрешает.
  4. Между двоеточием и значением — пробел. Host: api.example.com, не Host:api.example.com. Технически второе тоже валидно, но пробел — стандарт.

Анатомия HTTP-ответа

Ответ от сервера на запрос выше может выглядеть так:

HTTP/1.1 201 Created
Date: Thu, 14 May 2026 12:34:56 GMT
Content-Type: application/json
Content-Length: 89
Location: /api/users/42
Cache-Control: no-cache

{"id":42,"name":"Alice","age":30,"created_at":"2026-05-14T12:34:56Z"}
Ответ построчно
HTTP/1.1Версия протокола. Обычно та же, что и в запросе
201Статус-код. Числовой результат обработки. 201 = Created (ресурс создан)
CreatedТекстовая причина (reason phrase). Человекочитаемое описание кода. Браузер игнорирует, можно даже изменить на свой текст
DateВремя сервера в момент ответа. RFC 7231 формат. Используется для кэширования и debug
Content-TypeФормат тела. Тут JSON. Клиент по этому полю выбирает парсер
Content-LengthРазмер тела в байтах. Клиент знает, сколько ещё ждать данных
LocationURL созданного ресурса. По соглашению REST: при 201 Created Location указывает на новый ресурс
Cache-ControlИнструкции для клиента по кэшированию. no-cache = не кэшировать без проверки. Подробнее в модуле 3
Тело ответаJSON с созданным объектом. Сервер вернул всё, что есть, включая server-generated id и created_at

Reason phrase (Created) — это исторический рудимент. В HTTP/2 его уже нет (бинарный формат), но семантика та же: 201. Когда вы пишете API на Go или Python, фреймворк сам ставит правильный текст. Ставить кастомный (типа 420 Enhance Your Calm от Twitter) технически разрешено, но бессмысленно.


Реальный raw HTTP через nc (netcat)

TCP-сервер и клиент на Python: echo шаг за шагом

Самый честный способ увидеть HTTP — использовать nc (netcat), который просто открывает TCP-соединение и шлёт байты, ничего не зная про HTTP. Это дико полезно для отладки.

# Откройте TCP-соединение к httpbin.org:80 и руками отправьте HTTP-запрос:
printf 'GET /get HTTP/1.1\r\nHost: httpbin.org\r\nAccept: */*\r\nConnection: close\r\n\r\n' | nc httpbin.org 80

Что вы увидите — буквально HTTP-ответ от сервера, точно так, как он улетел по TCP:

HTTP/1.1 200 OK
Date: Thu, 14 May 2026 12:45:00 GMT
Content-Type: application/json
Content-Length: 312
Connection: close
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org"
  },
  "origin": "1.2.3.4",
  "url": "http://httpbin.org/get"
}

Заметьте:

  1. Connection: close — мы попросили сервер закрыть TCP-соединение после ответа. Без этого nc просто висел бы, ожидая больше данных. По умолчанию HTTP/1.1 keep-alive (соединение остаётся открытым для следующих запросов).
  2. \r\n — мы использовали printf с явными \r\n, потому что echo добавляет только \n. HTTP строго требует CRLF.
  3. Пустая строка\r\n\r\n в конце означает «конец заголовков, тела нет». Без этого сервер бы ждал ещё заголовков.
TIP

Метод печатания HTTP вручную через nc — лучшая отладка, когда что-то не работает в библиотеке. Если ваш Python-скрипт получает 400 Bad Request, а вы не понимаете, почему — выведите request объект (logging), скопируйте байт-в-байт и пошлите через nc. Если nc получит 200 — проблема в библиотеке. Если 400 — проблема в самом запросе.


То же самое через curl -v

curl -v показывает то же самое, только удобнее. Префиксы:

  • > — что мы отправили на сервер
  • < — что сервер прислал
  • * — debug-вывод от curl (TLS, DNS, и т.д., не часть HTTP)
curl -v https://httpbin.org/get

Пример вывода (упрощённо):

*   Trying 54.236.246.173:443...
* Connected to httpbin.org (54.236.246.173) port 443
* TLS handshake completed (TLS 1.3 / TLS_AES_256_GCM_SHA384)
> GET /get HTTP/2
> Host: httpbin.org
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/2 200
< date: Thu, 14 May 2026 12:50:00 GMT
< content-type: application/json
< content-length: 312
< server: gunicorn/19.9.0
<
{
  "args": {},
  "headers": {...},
  "origin": "...",
  "url": "https://httpbin.org/get"
}

Заметьте, что в HTTP/2 нет reason phrase (просто 200, без OK) — это бинарный фрейм, текстовый reason там не передаётся. Заголовки в HTTP/2 строчные (content-type, не Content-Type) — спецификация требует. На уровне семантики это та же информация.


Тело: что в нём может быть

Тело может содержать что угодно. Сервер сообщает формат через Content-Type. Самые частые:

Анатомия HTTP-запроса: что внутри request и response
Типичные Content-Type
application/jsonJSON. Самый популярный для REST API. Универсальный, читаемый, парсится во всех языках. Подробно -- модуль 4
application/xmlXML. Legacy формат, используется в SOAP и старых enterprise API. Тяжёлый, многословный
text/htmlHTML. Возвращают веб-страницы. Обычно для browser-based API мало релевантно DE
text/plainПросто текст. Используется для логов, простых ответов, error messages
application/x-www-form-urlencodedФормы. key1=value1&key2=value2. Браузеры по умолчанию шлют формы так. OAuth2 token endpoint требует именно это
multipart/form-dataФормы с файлами. Тело разбито на части с разделителями (boundary). Используется для upload файлов
application/octet-streamБинарные данные неизвестного типа. Файлы, blob-ы. Клиент сам решает, что с этим делать
application/x-ndjsonNewline-delimited JSON. Один JSON на строку. Удобно для стриминга больших датасетов. Подробно -- модуль 4

Есть также параметры в Content-Type. Например: application/json; charset=utf-8. Параметры идут через ;. Для JSON charset=utf-8 — стандарт по умолчанию (RFC 8259), но многие серверы и клиенты его всё равно указывают.


Content-Length и chunked encoding

Как клиент понимает, что тело закончилось? Два способа:

Способ 1: Content-Length. Сервер заранее говорит размер. Клиент читает столько байт и знает, что дальше — следующий ответ или конец.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 89

{"id":42,"name":"Alice","age":30,"created_at":"2026-05-14T12:34:56Z"}

Способ 2: Transfer-Encoding: chunked. Сервер не знает размер заранее (например, генерирует ответ потоково). Шлёт куски с указанием размера каждого.

HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

1c
{"event":"user_created"}
1f
{"event":"order_placed","id":1}
0

Каждый chunk — это размер в hex, потом данные, потом \r\n. Финальный chunk имеет размер 0 — это маркер конца. Полезно для long-polling, server-sent events (модуль 11), стриминга больших ответов.

WARNING

Если в одном ответе и Content-Length, и Transfer-Encoding: chunked — это уязвимость ‘HTTP request smuggling’. Современные серверы и прокси умеют детектировать и блокировать. Никогда не делайте так в собственных API.


Query string vs body

Где передавать данные — в URL или в теле? Зависит от метода и типа данных:

Параметры: query string или body
GET /search?q=python&page=2Параметры в query string (URL после ?). Подходят для GET. Плюсы: видно в логах, кэшируется, можно запинить URL. Минусы: ограничение по длине (~2-8 KB), всё видно
POST /api/users + bodyПараметры в теле запроса. Для POST/PUT/PATCH. Плюсы: нет ограничений по размеру, не светится в логах URL, поддерживает любые форматы (JSON, бинарка). Минусы: не кэшируется, не залинкуешь

Конвенция:

  • GET — параметры в query string. Тело технически разрешено, но в большинстве библиотек игнорируется.
  • POST / PUT / PATCH — данные в теле, обычно JSON.
  • DELETE — обычно ID ресурса в URL (/api/users/42), без тела.

Query string использует URL-encoding для специальных символов:

# Пробел становится %20 или +
# Кириллица -- UTF-8 в %XX
curl -G --data-urlencode 'q=привет мир' --data-urlencode 'page=2' \
  https://httpbin.org/get

Попробуй сам

Попробуйте все три способа увидеть HTTP:

# 1. Через nc (raw):
printf 'GET /get HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n' \
  | nc httpbin.org 80

# 2. Через curl -v (с TLS):
curl -v https://httpbin.org/get 2>&1 | head -25

# 3. POST с JSON-телом:
curl -v -X POST https://httpbin.org/post \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","age":30}'

# Посмотрите, как curl сам добавил Content-Length и Accept

# 4. То же через httpie (если установили):
http POST https://httpbin.org/post name=Alice age:=30
# httpie сам поставит Content-Type: application/json и сериализует

Откройте 2-3 терминала, поделайте разные варианты, посмотрите вывод. Через 10 минут такой практики у вас в голове сложится чёткая картина: запрос — это start-line + заголовки + тело, всё разделено CRLF.


Проверка знанийKnowledge check
Junior шлёт через nc такой запрос: 'GET /get HTTP/1.1\r\nHost: httpbin.org\r\n' (без второго \r\n в конце). Сервер не отвечает, nc висит. Что произошло и почему?
ОтветAnswer
Junior забыл отправить пустую строку в конце заголовков -- то есть второй CRLF, который сигнализирует серверу, что заголовки кончились. Структура HTTP-запроса: start-line + headers + ПУСТАЯ СТРОКА + (опциональное тело). Пустая строка -- это два байта \r\n, идущие сразу после CRLF последнего заголовка. Получается \r\n\r\n в конце -- это и есть маркер 'конец заголовков'. Без этого маркера сервер думает, что Junior ещё не дописал заголовки и продолжает ждать дальнейшие байты. nc держит TCP-соединение открытым, поэтому всё висит. Если бы Junior добавил Connection: close и финальный пустой CRLF -- сервер прочитал бы заголовки, обработал GET и ответил. Это классическая ошибка при первом написании HTTP руками, и она прекрасно иллюстрирует, насколько критична правильная структура сообщения. То же самое произойдёт, если в Python вы соберёте кастомный HTTP-запрос через сокет и забудете финальный \r\n\r\n.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какова правильная структура HTTP-сообщения (запрос или ответ)?

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

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

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

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