1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Из чего состоит запрос и ответ

Коротко

HTTP - это текст, который клиент и сервер шлют друг другу по установленному соединению. Правил в нём мало, и nginx настраивают ровно поверх этих правил.

  • Запрос и ответ устроены одинаково: строка, заголовки, пустая строка, тело. Пустая строка - не украшение, а признак «заголовки кончились».
  • Заголовок - это пара «имя: значение», по одной на строку. Регистр имени не важен, порядок не важен.
  • nginx дописывает в ответ то, чего ты не просил: Server, Date, Content-Length, ETag и ещё несколько.
  • Одно и то же имя может встретиться дважды, и в переменной nginx такие значения склеиваются через запятую.
  • Имя с подчёркиванием nginx по умолчанию выбрасывает молча.

Если четыре части сообщения и правило про подчёркивание знакомы - листай до «Теперь сам».

Сначала ответь сам

Клиент открыл соединение и отправил на сервер вот это, до последнего символа:

↳  выводчто ушло в сокет
GET /f.txt HTTP/1.1
Host: lab.local

Строка запроса на месте, Host на месте, всё написано верно. nginx молчит: ответа нет ни через секунду, ни через десять.

Чего не хватает?

Пустой строки. Пока она не пришла, сервер считает, что заголовки ещё идут, и честно ждёт продолжения. Добавь \r\n в конце - и ответ появится немедленно. Проверено: тот же запрос с пустой строкой отдаёт 200 OK за миллисекунды.

На что это похоже

Это бумажный бланк, который кладут в конверт.

Сверху одна главная строка: что вообще просят сделать. Ниже - поля: от кого, куда, чем оплачено, до какого числа действителен. Полей может быть три, а может быть тридцать, и порядок их не важен - важно, чтобы каждое было на своей строке.

Потом идёт отчёркивание, и оно значит «поля кончились, дальше приложение». Без него принимающий не знает, где заканчивается бланк и начинается вложенный документ, поэтому будет ждать.

А ниже отчёркивания - собственно груз: текст письма, картинка, файл. Он может быть, а может и не быть: у обычного GET его нет вовсе.

Механизм: четыре части, одинаковые у обеих сторон

запрос GET /f.txt HTTP/1.1 Host: lab.local User-Agent: curl/8.22 пустая строка тело - у GET его нет ответ HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 300 пустая строка тело - содержимое файла Первая строка у запроса и ответа разная: у запроса метод, путь и версия, у ответа версия, код и его название. Остальные три части устроены одинаково. Всё это - обычный текст, разделённый переводами строк.
nginx работает ровно с этими четырьмя частями: первую разбирает, вторую читает и переписывает, третью ищет как границу, четвёртую чаще всего просто пересылает.

Правило

Заголовки кончаются пустой строкой, а не концом данных. Всё, что nginx решает о запросе, он решает по первой строке и заголовкам - то есть до того, как увидит хоть байт тела.

Разбор: живой обмен целиком

Отправляем минимальный корректный запрос и смотрим, что приходит:

$  команда
printf 'GET /f.txt HTTP/1.1\r\nHost: lab.local\r\nConnection: close\r\n\r\n' | nc lab.local 80
↳  выводответ nginx 1.31.5 целиком, до пустой строки
HTTP/1.1 200 OK
Server: nginx/1.31.5
Date: Thu, 03 Sep 2026 16:30:16 GMT
Content-Type: text/plain
Content-Length: 300
Last-Modified: Thu, 03 Sep 2026 15:36:38 GMT
Connection: close
ETag: "6a999406-12c"
Accept-Ranges: bytes

Клиент попросил один файл и не просил ничего больше. nginx дописал восемь заголовков: назвал себя, поставил время, сказал тип и размер, дал два способа проверить свежесть (Last-Modified и ETag), сообщил, что умеет отдавать куски файла, и предупредил, что закроет соединение.

Каждый из них дальше в курсе окажется чьей-нибудь настройкой: Content-Type несёт MIME-типMIME-типСтрока вида text/html или image/png в заголовке Content-Type. По ней браузер решает, показать страницу, нарисовать картинку или предложить скачать файл. nginx берёт её из таблицы по расширению файла. и берётся из таблицы по расширению файла, ETag собирается из времени и размера файла, Server убирается директивой server_tokens off (проверено: остаётся просто nginx, без версии), а Connection зависит от того, кто попросил закрыть соединение.

Разбор: где наивное правило подводит

Регистр и лишние пробелы не значат ничего. hOsT: lab.local работает так же, как Host: lab.local. И перевод строки одним символом \n вместо пары \r\n nginx тоже принимает, хотя стандарт требует пару: серверы снисходительны к кривым клиентам, и на этом строится половина несовместимостей между реализациями.

Одно имя может встретиться дважды. Клиент вправе прислать X-Dup: a и X-Dup: b двумя строками. Бэкенду nginx передаст обе, а вот в своей переменной склеит через запятую:

↳  выводreturn 200 "dup=[$http_x_dup]" при двух заголовках X-Dup
dup=[a, b]

Значит проверка вида «если заголовок равен a» на таком запросе не сработает, и это классический способ обойти наивную проверку в конфиге.

Имя с подчёркиванием исчезает молча. Отправим X-My_Header: v1:

↳  выводreturn 200 "x_my_header=[$http_x_my_header]"
x_my_header=[]

Заголовка нет ни в переменной, ни в том, что уедет бэкенду. Так задумано: подчёркивание в имени легко путается с дефисом (в переменных nginx они неразличимы - $http_x_my_header читает и X-My-Header, и X-My_Header), а через сломанные обёртки такие имена служили способом протащить чужой заголовок. Включается директивой underscores_in_headers on на уровне server:

/etc/nginx/conf.d/shop.local.confhttp server
underscores_in_headers on;

Проверено: с ней тот же запрос даёт x_my_header=[v1].

Что ломается без этого

Первый способ потерять день - искать причину в приложении, когда её отрезал nginx. Заголовок X-Api_Key, который «точно отправляется», до бэкенда не доезжает, и в его логах пусто. Строки в error_log тоже нет: это не ошибка, это поведение по умолчанию.

Второй - писать в конфиге проверки по заголовку, не помня, что клиент управляет и его регистром, и его количеством.

Зачем это в работе

Смотреть обмен целиком удобнее всего самим curl:

$  команда
curl -v https://shop.local/api/cart

Строки со > - то, что ушло; со < - то, что пришло. Это первое, что просят показать при разборе любого «у нас не работает», и по этим двум блокам видно сразу: дошёл ли Host, какой код вернулся, кто поставил Content-Type.

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

$  команда
curl -sI https://shop.local/app.js

Метод HEAD просит сервер ответить так же, как на GET, но без тела. Проверено: Content-Length: 300 в ответе есть, а байтов приходит ноль. То есть узнать размер и тип файла можно, не качая его.

Теперь сам

Клиент отправляет заголовок X-Request_Source: mobile, приложение его не видит. В error_log пусто, в access_log код 200. Где искать?

Ни в приложении, ни в логах - искать нечего, строки об этом не будет. Имя содержит подчёркивание, и nginx выбросил заголовок ещё на разборе запроса. Два выхода: попросить клиента переименовать его в X-Request-Source (это правильный путь - дефис привычнее и не зависит от настроек сервера) либо поставить underscores_in_headers on в нужном server.

Главное

Запрос и ответ - это текст из четырёх частей: строка, заголовки, пустая строка, тело. Пустая строка означает «заголовки кончились», и без неё сервер ждёт. Заголовок - пара «имя: значение»; регистр имени и порядок не важны, одно имя может повториться, и в переменной nginx такие значения склеиваются запятой. В ответ nginx кладёт восемь заголовков от себя, включая Server, Content-Length и ETag. Имя с подчёркиванием отбрасывается молча - лечится underscores_in_headers on.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.