# теория · шаг 1 из 7
Из чего состоит запрос и ответ
Коротко
HTTP в версии 1.1 - это текст, который клиент и сервер шлют друг другу по установленному соединению; более новые версииРазбирается в разделе 10, глава «HTTP/2 сегодня»: Что HTTP/2 меняет на самом деле передают те же запросы и ответы двоичными кадрами. Правил в нём мало, и nginx настраивают ровно поверх этих правил.
- Запрос и ответ устроены одинаково: строка, заголовки, пустая строка, тело. Пустая строка - не украшение, а признак «заголовки кончились».
- Заголовок - это пара «имя: значение», по одной на строку. Регистр имени не важен, порядок не важен.
Если четыре части сообщения и правило про подчёркивание знакомы - листай до «Теперь сам».
Сначала ответь сам
Клиент открыл соединение и отправил на сервер вот это, до последнего символа:
GET /f.txt HTTP/1.1
Host: lab.local
Строка запроса на месте, Host на месте, всё написано верно. nginx молчит:
ответа нет ни через секунду, ни через десять.
Чего не хватает?
Пустой строки. Пока она не пришла, сервер считает, что заголовки ещё идут, и
честно ждёт продолжения. Добавь \r\n в конце - и ответ появится немедленно.
Проверено: тот же запрос с пустой строкой отдаёт 200 OK за миллисекунды.
На что это похоже
Это бумажный бланк, который кладут в конверт.
Сверху одна главная строка: что вообще просят сделать. Ниже - поля: от кого, куда, чем оплачено, до какого числа действителен. Полей может быть три, а может быть тридцать, и порядок их не важен - важно, чтобы каждое было на своей строке.
Потом идёт отчёркивание, и оно значит «поля кончились, дальше приложение». Без него принимающий не знает, где заканчивается бланк и начинается вложенный документ, поэтому будет ждать.
А ниже отчёркивания - собственно груз: текст письма, картинка, файл. Он может
быть, а может и не быть: у обычного GET его нет вовсе.
Механизм: четыре части, одинаковые у обеих сторон
Правило
Заголовки кончаются пустой строкой, а не концом данных. Сервер и location nginx выбирает по первой строке и заголовкам - то есть до того, как увидит хоть байт тела. Тело читается потом, и часть решений принимается уже по нему: если длина заранее не объявлена, превышение лимита на размер тела выясняется только по ходу чтения.
Разбор: живой обмен целиком
Отправляем минимальный корректный запрос и смотрим, что приходит:
printf 'GET /f.txt HTTP/1.1\r\nHost: lab.local\r\nConnection: close\r\n\r\n' | nc lab.local 80
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 передаст обе, а вот в своей переменной
склеит через запятую:
dup=[a, b]
Значит проверка вида «если заголовок равен a» на таком запросе не сработает, и
это классический способ обойти наивную проверку в конфиге.
Имя с подчёркиванием исчезает молча. Отправим X-My_Header: v1:
x_my_header=[]
Заголовка нет ни в переменной, ни в том, что уедет бэкенду. Так задумано:
подчёркивание в имени легко путается с дефисом (в переменных nginx они
неразличимы - $http_x_my_header читает и X-My-Header, и X-My_Header), а
через сломанные обёртки такие имена служили способом протащить чужой заголовок.
Включается директивой underscores_in_headers on на уровне 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
МетодМетодПервое слово в строке запроса: GET - дай, POST - прими, HEAD - ответь как на GET, но без тела, DELETE - удали. Сервер вправе обрабатывать разные методы по-разному, а nginx умеет их различать директивой limit_except. HEAD просит сервер ответить так же, как на GET, но без тела. Проверено:
Content-Length: 300 в ответе есть, а байтов приходит ноль. То есть узнать
размер и тип файла можно, не качая его.
Теперь сам
Клиент отправляет заголовок X-Request_Source: mobile, приложение его не видит.
В error_log пусто, в access_log код 200. Где искать?
Ни в приложении, ни в логах - искать нечего, строки об этом не будет. Имя
содержит подчёркивание, и nginx выбросил заголовок ещё на разборе запроса. Два
выхода: попросить клиента переименовать его в X-Request-Source (это правильный
путь - дефис привычнее и не зависит от настроек сервера) либо поставить
underscores_in_headers on в нужном server.
Главное
В HTTP/1.1 отделяй стартовую строку и заголовки от тела: границу отмечает пустая строка. При отладке сравни запрос и ответ через curl -v. Так видно, что пришло от клиента, какие заголовки добавил сервер и почему чтение ещё не завершилось.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий