Что такое HTTP: руководство для системного администратора
Разбор протокола HTTP для администраторов серверов. Изучите структуру запросов, коды состояния, заголовки и логи nginx. Узнайте, как работают HTTP/3 и TLS в реальной среде.
Что такое HTTP?
HTTP (hypertext transfer protocol) — это набор правил, которые клиент и веб-сервер используют для запроса и передачи данных. Клиент отправляет запрос: метод, например GET, путь, например /pricing, версию протокола, список заголовков и иногда тело запроса. Сервер отвечает кодом состояния, например 200, за которым следуют его собственные заголовки и, как правило, тело ответа. Каждый просмотр страницы и каждый вызов API (application programming interface) на вашем сервере — это один и тот же повторяющийся обмен данными.
HTTP не сохраняет состояние. Сервер не «помнит», что вы запрашивали секунду назад, поэтому всё, что имитирует память (например, сессия входа в систему), передаётся в заголовке каждого отдельного запроса. Это свойство объясняет многое из того, что будет описано далее: кэширование полностью управляется заголовками, а балансировщик нагрузки может отправить ваш следующий запрос на другой backend, не нарушая работу приложения.
Всё, что приведено ниже, описывает, как эта модель выглядит со стороны сервера: в вашем access log и в конфигурации nginx.
Пример необработанного запроса и ответа с пояснениями
Ниже приведен полный HTTP/1.1 запрос. Пустая строка завершает заголовки, всё, что идет после неё, является телом запроса. У GET обычно нет тела.
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGET— это метод, определяющий желаемое действие.GETсчитывает данные,POSTотправляет их,PUTзаменяет ресурс,DELETEудаляет его,HEADзапрашивает только заголовкиGETбез тела./pricing— это путь. Имя хоста не является частью строки запроса, поэтому необходим следующий заголовок.HTTP/1.1— это версия протокола, которую использует клиент.Host: example.comуказывает сайт, к которому обращается клиент. HTTP/1.1 требует этот заголовок, поэтому Nginx отвечает на запрос без него кодом400 Bad Request.- Остальные строки — это предпочтения.
Accept-Encoding: gzipсообщает, что клиент поддерживает распаковку, поэтому сервер может сжимать тело ответа.
Ответ имеет ту же структуру, но начинается со строки состояния.
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OK— это код состояния и его текстовое описание. Важен именно код; описание является пояснением, которое клиенты игнорируют.Content-Typeуказывает клиенту, как интерпретировать последующие байты.Content-Length— это размер тела в байтах, чтобы клиент знал, где оно заканчивается. Если размер заранее неизвестен, сервер отправляетTransfer-Encoding: chunkedи помечает конец чанком нулевой длины.Cache-Controlсообщает браузеру и промежуточным кэшам, как долго ответ считается актуальным. По истеченииmax-ageсекунд он считается устаревшим. Это не означает удаление: кэш может хранить устаревшую копию и выполнить её повторную валидацию на сервере вместо повторной загрузки всего тела.- Пустая строка после заголовков отделяет их от тела в обоих направлениях.
Имена заголовков нечувствительны к регистру, каждая строка завершается символами возврата каретки и перевода строки, а не просто символом переноса строки. Вы не будете вводить их вручную, но встретите в дампе пакетов.
Чтобы увидеть реальную пару запроса и ответа, выполните следующую команду для своего сайта:
curl -sS -o /dev/null -D - https://example.com/-D - выводит заголовки ответа в терминал, а -o /dev/null отбрасывает тело. Используйте этот вариант вместо curl -I, так как -I отправляет запрос HEAD. Сервер приложений, который обрабатывает HEAD иначе, чем GET (а таких много), покажет вам заголовки, которые никогда не увидит браузер. curl -v выводит обе стороны, при этом строки запроса помечены >, а строки ответа — <.
Как выглядит строка запроса в access log вашего nginx
nginx поставляется с форматом логов combined, вот его определение:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';Одна строка, созданная этим форматом:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45— это$remote_addr, адрес, который открыл TCP (transmission control protocol) соединение. Если перед вами прокси, здесь будет адрес прокси, а не посетителя, если только вы не настроили модуль realip, описанный ниже, для подмены этого значения.- Первый
-— это фиксированный заполнитель. Второй —$remote_user, который заполняется только при использовании HTTP basic authentication. "GET /pricing HTTP/1.1"— это$request, полная исходная строка запроса. При стандартном экранировании логов в nginx двойные кавычки, обратная косая черта и любые байты со значением менее 32 или более 126 записываются как\xXX, поэтому строка передает смысл, но не всегда совпадает байт в байт.200— это статус, который вернул ваш сервер, а не статус, который увидел посетитель.5310— это$body_bytes_sent, только тело ответа. Заголовки ответа не учитываются, поэтому это число всегда меньше, чем количество фактически отправленных байтов.- Последние два поля в кавычках — это
RefererиUser-Agent. Оба значения приходят от клиента, поэтому они могут содержать любые данные.
Мусор также записывается в лог в экранированном виде. Клиент, который пытается использовать TLS (transport layer security) на вашем порту 80 для обычного текста, оставляет строку 400, в которой поле запроса начинается с экранированных байтов, таких как "\x16\x03\x01\x02\x00\x01". \x16 — это тип записи TLS handshake, поэтому эти байты являются началом ClientHello, а не строкой запроса. Ваш сервер работает корректно. Кто-то направляет HTTPS-трафик на HTTP-порт.
Добавьте $server_protocol в ваш формат логов. Он выводит HTTP/1.1, HTTP/2.0 или HTTP/3.0, и это самый быстрый способ убедиться, что изменение протокола действительно вступило в силу.
Что означают распространенные коды состояния, когда ваш сайт их возвращает
Первая цифра указывает на класс, и именно с него следует начинать анализ.
2xx означает успех. 200 OK при обычном чтении. 201 Created после запроса POST, в результате которого был создан объект. 204 No Content при успешном выполнении без данных для отправки, что является стандартным ответом на DELETE.
3xx означает перенаправление. 301 является постоянным, и браузеры кэшируют его очень агрессивно, иногда до тех пор, пока пользователь не очистит профиль, поэтому 301 с неверным именем хоста сложно исправить. Используйте 302, пока вы еще тестируете редирект. 304 Not Modified — это успех, а не ошибка: клиент отправил условный запрос, либо If-None-Match с ETag (entity tag), который вы все еще распознаете, либо If-Modified-Since с датой, после которой ресурс не менялся, поэтому вы ответили только заголовками без тела ответа. Журнал, заполненный 304, означает, что кэширование работает.
4xx означает, что запрос составлен неверно. 400 Bad Request — это некорректный ввод. 401 Unauthorized буквально означает отсутствие аутентификации, и такой ответ должен содержать заголовок WWW-Authenticate с указанием схемы. 403 Forbidden означает, что запрос был понят, но всё равно отклонен. 404 Not Found — это путь, который не существует, либо путь, который сервер предпочитает не признавать существующим, так как некоторые приложения отвечают 404 вместо 403, чтобы скрыть ресурс от клиентов без доступа. 405 Method Not Allowed — это верный путь с неверным методом, что возвращается при попытке POST к расположению статического файла. 413 означает, что тело запроса больше, чем значение client_max_body_size в nginx, которое по умолчанию составляет 1 мегабайт, и журнал ошибок подтверждает это записью client intended to send too large body.
403 для статического файла почти всегда связано с файловой системой, а не с правилами HTTP. Прочитайте /var/log/nginx/error.log перед изменением конфигурации. open() "/srv/site/index.html" failed (13: Permission denied) означает, что пользователь, от имени которого работает рабочий процесс nginx, не может прочитать файл, чаще всего из-за отсутствия прав на выполнение (execute) для других пользователей у родительской директории. directory index of "/srv/site/" is forbidden означает, что путь указывает на директорию, в которой нет индексного файла, в то время как autoindex отключен.
5xx означает, что проблема на вашей стороне. 500 — это общая ошибка сервера, и на вашем сайте она обычно указывает на необработанное исключение в приложении. 502 Bad Gateway означает, что nginx не смог получить корректный ответ от апстрима, и журнал ошибок указывает причину: connect() failed (111: Connection refused) while connecting to upstream означает, что по адресу в proxy_pass никто не слушает. 504 Gateway Timeout означает, что nginx перестал ждать ответа от апстрима. Обычно это происходит, когда апстрим принял соединение, но ничего не ответил в течение proxy_read_timeout (по умолчанию 60 секунд), что фиксируется в журнале как upstream timed out (110: Connection timed out) while reading response header from upstream. Попытка соединения, которая не завершилась в течение proxy_connect_timeout (также 60 секунд по умолчанию), также заканчивается 504, и строка журнала в этом случае заканчивается на while connecting to upstream. 503 Service Unavailable означает, что сервер временно не может обработать запрос, обычно из-за перегрузки или планового обслуживания. Обратите внимание, что встроенный ограничитель частоты запросов (rate limiter) nginx возвращает 503, так как limit_req_status по умолчанию имеет значение 503. Если вы ищете 429 Too Many Requests в журнале, а находите 503, причина именно в этом. Установите limit_req_status 429;, чтобы получать точный код.
Заголовки, важные при работе сервера
Host определяет сайт. Один IP-адрес может обслуживать сотни имен хостов, и nginx сопоставляет Host с server_name, чтобы решить, какой блок server будет отвечать. Если совпадений нет, nginx использует сервер по умолчанию — это первый блок, который слушает данный адрес и порт, если только другой не помечен как default_server. Если при добавлении нового виртуального хоста вы получаете не тот сайт, почти всегда причина в этом: имя не совпало, и запрос был перенаправлен на сервер по умолчанию. Проверьте это, не меняя DNS:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent — это самоописание, которое предоставляет клиент; это произвольный текст. Используйте его как подсказку при чтении логов. Никогда не используйте его для контроля доступа, так как клиент, который хочет солгать, просто сделает это. Фильтрация скрейперов по User-Agent отсеивает только вежливых ботов.
Content-Type определяет, как интерпретировать байты: application/json для API-запроса, text/html; charset=utf-8 для страницы. nginx сопоставляет расширения файлов с типами через /etc/nginx/mime.types, а стандартный файл nginx.conf задает default_type application/octet-stream;. Если nginx не знает расширение файла, он предложит его скачать вместо отображения. Видимый симптом — страница загружается без стилей, а в консоли браузера выводится Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME расшифровывается как Multipurpose Internet Mail Extensions — это схема именования, из которой произошли данные строки типов.
Cache-Control позволяет управлять всеми кэшами между вашим сервером и читателем. public, max-age=31536000, immutable подходит для ресурсов, в имени файла которых содержится хеш контента, так как имя меняется при изменении содержимого. no-store следует использовать для всего, что привязано к конкретному пользователю, иначе общий кэш может выдать страницу авторизованного пользователя следующему посетителю, запросившему тот же URL. private — промежуточный вариант: браузер может сохранить файл, а общий кэш — нет.
X-Forwarded-For существует, потому что прокси скрывает посетителя. Как только запрос проходит через reverse proxy, $remote_addr становится адресом прокси, поэтому ваши логи, геолокация и rate limiting видят только одного клиента, пока вы не восстановите исходный адрес. Прокси должен передать исходный адрес дальше:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;Принимающий сервер должен быть настроен так, чтобы доверять этому заголовку, и должен точно знать, кому именно доверять:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;Эти две директивы относятся к модулю realip в nginx. Они заменяют $remote_addr на переданный адрес, сохраняя собственный адрес соединения в $realip_remote_addr. Если вы собираете nginx самостоятельно, добавьте --with-http_realip_module, так как этот модуль не включен по умолчанию.
Указывайте только те диапазоны, которые вы контролируете. X-Forwarded-For — это обычный текст, который может отправить любой клиент, поэтому set_real_ip_from 0.0.0.0/0; позволяет посетителю самому выбрать адрес, который вы запишете в логи и который будет учитывать ваш rate limiter.
X-Forwarded-Proto предотвращает специфическую и очень распространенную ошибку. Ваш прокси завершает TLS и пересылает запрос приложению по обычному HTTP. Приложение видит обычный запрос, решает, что посетитель должен использовать HTTPS, и отвечает 301 https://example.com/. Браузер следует редиректу, прокси снова завершает TLS и снова пересылает HTTP — цикл повторяется, пока браузер не выдаст ошибку ERR_TOO_MANY_REDIRECTS. Отправка X-Forwarded-Proto: https сообщает приложению, что посетитель уже использует HTTPS, поэтому оно прекращает перенаправления.
HTTP/1.1, HTTP/2 и HTTP/3: что меняется для вас
HTTP/1.1 — это текстовый протокол, который на практике обрабатывает по одному запросу за раз на каждое соединение. Connection: keep-alive позволяет повторно использовать одно TCP-соединение для следующих запросов, что экономит ресурсы на установку связи, но ответы всё равно приходят в порядке их запроса. Протокол также поддерживает конвейеризацию (pipelining), когда клиент отправляет несколько запросов, не дожидаясь ответов, но правило очерёдности сохраняется, и ни один современный браузер не включает конвейеризацию по умолчанию. Один медленный ответ блокирует всё, что стоит в очереди за ним. Это называется блокировкой начала очереди (head-of-line blocking), и браузеры обходят её, открывая сразу несколько соединений к одному хосту.
HTTP/2 сохраняет те же методы и коды состояния, но меняет формат передачи данных на бинарный. Множество запросов используют одно соединение как независимые потоки, а повторяющийся текст заголовков сжимается, что важно, так как современные запросы содержат их в большом количестве. Соединение по-прежнему использует TCP, поэтому потеря одного пакета приостанавливает все потоки внутри этого соединения до тех пор, пока не придет повторно переданный пакет. Блокировка начала очереди не исчезла, она переместилась с уровня HTTP на транспортный уровень. Server push был частью HTTP/2, но на практике он не используется, так как Chrome убрал его поддержку в 2022 году.
HTTP/3 сохраняет ту же семантику, но заменяет TCP на QUIC — транспортный протокол, построенный поверх UDP (user datagram protocol). Потоки QUIC полностью независимы, поэтому потеря пакета приостанавливает только тот поток, к которому он относился. TLS 1.3 встроен в рукопожатие QUIC, а не накладывается поверх, поэтому для нового соединения требуется меньше обменов данными (round trips). Из этого следуют два практических вывода: UDP-порт, который ваш сервер использует для HTTP/3, должен быть открыт во всех межсетевых экранах на пути следования, а любая сеть, ограничивающая или блокирующая UDP, заставит клиентов вернуться к HTTP/2. Обычно это порт 443, но протокол позволяет серверу использовать любой UDP-порт, так как объявление, указывающее браузерам на HTTP/3, всегда явно называет порт.
Что меняется для вас конкретно. Браузеры обычно не начинают работу с HTTP/3. Они подключаются по HTTP/2 или HTTP/1.1, видят заголовок Alt-Svc: h3=":443"; ma=86400 в ответе и используют HTTP/3 для последующих соединений с этим хостом. Исключение составляет запись HTTPS в DNS, значение alpn которой содержит h3, что позволяет браузеру, например Firefox, открыть HTTP/3 уже при первом соединении. Эта запись — дополнительная подсказка, а не замена, поэтому заголовок не является необязательным украшением. Это основной механизм обнаружения. В nginx HTTP/2 стал отдельной директивой в версии 1.25.1 (http2 on; внутри блока server, заменяя старый параметр listen ... http2), а QUIC появился в основной ветке 1.25.0, где для сайта на HTTP/3 требуется listen 443 quic reuseport; наряду с обычным listen 443 ssl;. Модуль HTTP/3 не собирается по умолчанию (--with-http_v3_module), и документация nginx до сих пор называет его поддержку экспериментальной.
Прокси-серверы различаются по уровню зрелости, и это стоит проверять в зависимости от версии, которую вы используете. По состоянию на ноябрь 2026 года Caddy обслуживает HTTP/3 по умолчанию без дополнительной настройки. nginx требует явного слушателя quic плюс заголовок Alt-Svc, описанный выше. Traefik включает его для каждой точки входа через явную опцию http3. Если вы завершаете TLS на Traefik перед несколькими Docker-приложениями, версия протокола, которую получают посетители, определяется именно там, а переход от прокси к вашему контейнеру обычно осуществляется по обычному HTTP/1.1, независимо от того, о чём договорился браузер.
Проверяйте, а не предполагайте. curl --http3 -sS -o /dev/null -D - https://example.com/ работает только в том случае, если curl -V содержит HTTP3 в списке своих функций, а большинство сборок в дистрибутивах не включают его. Самый надежный способ проверки — ваш собственный лог: добавьте $server_protocol в формат и посмотрите, что именно согласовывают реальные браузеры. Прежде всего, убедитесь, что UDP-порт, который использует ваш слушатель quic (443 в конфигурации выше), действительно открыт, так как межсетевой экран, разрешающий только TCP 443, приведет к тому, что HTTP/3 будет молча сбоить, пока сайт продолжит работать через HTTP/2. Знание того, какие порты открыты и прослушиваются на вашем Linux-сервере, — это первое, что нужно проверить.
HTTPS: HTTP — это протокол, TLS — это оболочка
HTTPS не является отдельным протоколом. Это те же самые запросы и те же коды состояния, передаваемые внутри TLS-сессии. Порт 80 передает их в открытом виде, а порт 443 — в зашифрованном. Сначала завершается TLS-рукопожатие, затем HTTP-запрос передается внутри зашифрованного канала. Именно из-за этого порядка у проблем с сертификатом никогда не бывает кода состояния: сбой происходит до отправки первого байта HTTP, поэтому нет ответа, которому можно было бы присвоить номер.
При размещении нескольких сайтов на одном сервере важен один нюанс порядка действий. Сертификат выбирается с помощью SNI (server name indication) — поля в TLS-рукопожатии, которое передает имя хоста в открытом виде еще до появления каких-либо HTTP-заголовков. Таким образом, сервер сначала выбирает сертификат на основе SNI, а затем выбирает виртуальный хост на основе заголовка Host. Это два отдельных поиска, которые обычно совпадают. Когда они не совпадают, браузер показывает ошибку несоответствия имени, например NET::ERR_CERT_COMMON_NAME_INVALID, и вообще не отправляет запрос, так как сертификат вашего сервера по умолчанию был предложен для имени, которое он не покрывает.
Для публичного сайта получите настоящий сертификат и настройте его автоматическое обновление. Certbot с Let's Encrypt на nginx записывает пути к сертификатам в ваш server block и устанавливает таймер для обновления. Для имени хоста, которое не может подтвердить ни один публичный центр сертификации (например, внутреннее имя или IP-адрес в вашей сети), самоподписанный сертификат на Ubuntu является честным решением, при условии, что вы готовы к необходимости вручную подтверждать доверие к нему на каждом клиенте.
Как только TLS заработает, перенаправляйте весь трафик с 80 порта на 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Добавляйте Strict-Transport-Security только тогда, когда вы полностью уверены в настройках. Заголовок add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; указывает браузерам отказываться от обычного HTTP для этого имени хоста в течение двух лет, и они соблюдают это правило, используя собственный кэш, поэтому удаление заголовка позже не отменит это действие. Начните с max-age на несколько часов, убедитесь, что каждый поддомен действительно работает по HTTPS, и только после этого увеличивайте значение.
FAQ
В чем разница между HTTP и HTTPS?
HTTPS — это HTTP, передаваемый внутри сессии TLS (transport layer security). Методы и коды состояния идентичны. Разница заключается в том, что байты шифруются между клиентом и точкой завершения TLS, а порт по умолчанию меняется с 80 на 443. Поскольку рукопожатие TLS завершается до отправки первого байта HTTP, ошибка сертификата никогда не приводит к коду состояния. Именно поэтому предупреждение браузера о сертификате показывает название ошибки, например NET::ERR_CERT_COMMON_NAME_INVALID, а не число вроде 403.
Почему мой сайт возвращает 502 Bad Gateway?
Код 502 от nginx означает, что nginx не смог получить корректный ответ от апстрима, на который он проксирует запрос. Это значит, что запрос пользователя был в порядке, а проблема возникла на стороне сервиса за nginx. Изучите /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream означает, что по адресу и порту, указанным в proxy_pass, никто не слушает запросы. Проверьте, что приложение запущено и привязано к ожидаемому адресу. no live upstreams while connecting to upstream означает, что все серверы в блоке апстрима были помечены как нерабочие после серии сбоев. Сравните это с 504 Gateway Timeout: nginx превысил время ожидания ответа от апстрима — либо при установке соединения в течение proxy_connect_timeout, либо при ожидании ответа в течение proxy_read_timeout.
Почему в логе доступа для всех посетителей отображается один и тот же IP-адрес?
Потому что $remote_addr записывает адрес, который открыл TCP-соединение, а при использовании reverse proxy или сети доставки контента этим адресом является сам прокси. Адрес посетителя передается в заголовке X-Forwarded-For. Установите proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; на прокси, а затем на принимающем nginx настройте set_real_ip_from на диапазон адресов прокси и real_ip_header X-Forwarded-For;. Указывайте только те диапазоны, которые вы контролируете: этот заголовок — обычный текст, который может отправить любой клиент. Доверие к нему со стороны всего интернета позволит посетителю подменять адрес, который вы логируете и по которому ограничиваете частоту запросов.
Нужно ли включать HTTP/2 или HTTP/3?
HTTP/2 стоит включить: это одна директива для сайта, где уже настроен TLS, и она снимает ограничение на количество запросов в одном соединении, из-за которого страницы с множеством мелких файлов загружаются медленно. HTTP/3 дает меньший и менее очевидный прирост производительности, при этом требуя открытого UDP-порта (обычно 443) и сборки прокси с поддержкой QUIC. Помните, что браузеры обычно переключаются на HTTP/3 только после получения заголовка Alt-Svc в предыдущем ответе или записи HTTPS DNS, в которой указан h3. Без этого ничего не изменится, что бы ни было написано в вашей строке listen. Добавьте $server_protocol в формат логов и оцените, что именно используют посетители, прежде чем тратить время на настройку.
Что означает 403 Forbidden, если файл существует?
На статическом сайте 403 обычно указывает на права доступа в файловой системе, а не на правила HTTP. open() ... failed (13: Permission denied) в /var/log/nginx/error.log означает, что пользователь, от имени которого работает nginx, не может прочитать файл. Чаще всего это происходит из-за того, что у родительского каталога отсутствуют права на выполнение для остальных пользователей, а не из-за неверных прав на сам файл. directory index of ... is forbidden означает, что запрос был направлен к каталогу без индексного файла, в то время как autoindex выключен. Явное правило deny в соответствующем блоке location также возвращает 403, поэтому изучите этот блок, если в логе ошибок нет информации.