SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Что такое 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: gzip
  • GET — это метод, определяющий требуемое действие. 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 сообщает браузеру и промежуточным кэшам, как долго можно хранить этот ответ.
  • Пустая строка после заголовков отделяет их от тела в обоих направлениях.

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

Чтобы увидеть реальную пару запрос-ответ, выполните команду для своего сайта:

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) соединение. Если используется прокси, здесь будет адрес прокси, а не посетителя.
  • Первый - — это фиксированный заполнитель. Второй — $remote_user, который заполняется только при использовании HTTP basic authentication.
  • "GET /pricing HTTP/1.1" — это $request, строка запроса, скопированная в точности так, как она поступила.
  • 200 — это статус, который вернул ваш сервер, а не тот, который увидел посетитель.
  • 5310 — это $body_bytes_sent, только тело ответа. Заголовки ответа не учитываются, поэтому это число всегда меньше, чем количество фактически переданных байтов.
  • Последние два поля в кавычках — это Referer и User-Agent. Оба приходят от клиента, поэтому могут содержать любые данные.

Поскольку $request копируется дословно, «мусор» также отображается дословно. Клиент, который пытается использовать 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), который вы всё ещё распознаете, поэтому вы ответили только заголовками без тела. Журнал, заполненный 304, означает, что кэширование работает.

4xx означает ошибку в запросе. 400 Bad Request — неверный формат входных данных. 401 Unauthorized буквально означает отсутствие аутентификации, и такой ответ должен содержать заголовок WWW-Authenticate с указанием схемы. 403 Forbidden означает, что запрос был понят, но в его выполнении отказано. 404 Not Found — путь не существует. 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 не получил корректный ответ от upstream, и журнал ошибок указывает причину: connect() failed (111: Connection refused) while connecting to upstream означает, что по адресу в proxy_pass никто не слушает порт. 504 Gateway Timeout означает, что upstream принял соединение, но не ответил в течение proxy_read_timeout (по умолчанию 60 секунд), что фиксируется в журнале как upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable — намеренный отказ в соединении. Обратите внимание, что встроенный ограничитель частоты запросов 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;

Указывайте только те диапазоны, которые вы контролируете. 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-соединение для последующих запросов, что экономит ресурсы на установку соединения, но ответы всё равно приходят в порядке их отправки. Один медленный ответ блокирует всё, что стоит в очереди за ним. Это называется 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-порт 443 должен быть открыт на всех межсетевых экранах на пути следования трафика, а любая сеть, которая ограничивает или блокирует UDP, заставит клиентов вернуться к HTTP/2.

Что меняется для вас конкретно. Браузеры никогда не начинают работу с HTTP/3. Они подключаются по HTTP/2 или HTTP/1.1, видят заголовок Alt-Svc: h3=":443"; ma=86400 в ответе и используют 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;.

Уровень зрелости поддержки в прокси-серверах различается, и это стоит проверять в зависимости от версии, которую вы используете. По состоянию на август 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-порт 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 не смог получить корректный ответ от upstream-сервера, на который он проксирует запрос. Запрос посетителя был корректен, но проблема возникла на стороне приложения за nginx. Изучите /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream означает, что по адресу и порту, указанным в proxy_pass, никто не слушает соединения. Проверьте, что приложение запущено и привязано к ожидаемому адресу. no live upstreams while connecting to upstream означает, что все серверы в блоке upstream были помечены как нерабочие после серии сбоев. Сравните это с 504 Gateway Timeout: это означает, что upstream принял соединение, но не успел ответить в течение proxy_read_timeout.

Почему в логе доступа для всех посетителей указан один и тот же IP-адрес?

Потому что $remote_addr записывает адрес, который открыл TCP-соединение. При использовании reverse proxy или сети доставки контента (CDN) этим адресом является сам прокси. Адрес посетителя передается в заголовке 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 в предыдущем ответе. Без этого заголовка ничего не изменится, что бы ни было написано в вашей строке listen. Добавьте $server_protocol в формат лога и измерьте, что именно используют посетители, прежде чем тратить время на настройку.

Что означает 403 Forbidden, если файл существует?

На статическом сайте ошибка 403 чаще связана с правами доступа в файловой системе, а не с правилами HTTP. open() ... failed (13: Permission denied) в /var/log/nginx/error.log означает, что пользователь, от имени которого работает nginx, не может прочитать файл. Чаще всего это происходит из-за отсутствия прав на выполнение (execute) у родительской директории для остальных пользователей, а не из-за прав на сам файл. directory index of ... is forbidden означает, что запрос был направлен к директории, в которой нет индексного файла, а autoindex отключен. Явное правило deny в соответствующем блоке location также возвращает 403, поэтому проверяйте этот блок, если в логе ошибок нет информации.

#http#https#web-server#headers#http3