Що таке HTTP: посібник для адміністраторів серверів
Пояснення HTTP для адміністраторів: методи, коди стану, важливі заголовки, журнали nginx, а також місце HTTP/3 і TLS у стеку.
Що таке HTTP?
HTTP (протокол передавання гіпертексту) — це набір правил, за якими клієнт і вебсервер запитують і передають дані. Клієнт надсилає запит: метод, наприклад GET, шлях, наприклад /pricing, версію протоколу, список заголовків і, за потреби, тіло запиту. Сервер відповідає кодом стану, наприклад 200, а потім надсилає власні заголовки та зазвичай тіло відповіді. Кожен перегляд сторінки та кожен виклик API (інтерфейсу прикладного програмування) на вашому сервері — це такий обмін, що повторюється.
HTTP не зберігає стану. Сервер не пам’ятає, що ви запитували секунду тому, тому все, що має поводитися як пам’ять, наприклад сеанс входу, передається в заголовку кожного окремого запиту. Ця властивість пояснює багато з описаного далі: кешування повністю керується заголовками, а балансувальник навантаження може надіслати наступний запит до іншого бекенду без порушення роботи.
Усе нижче показує, як ця модель виглядає на стороні сервера — у журналі доступу та конфігурації 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повідомляє браузеру та будь-якому проміжному кешу, як довго можна зберігати цю відповідь.- Порожній рядок після заголовків відділяє їх від тіла в обох напрямках.
Назви заголовків нечутливі до регістру. Кожен рядок завершується поверненням каретки, за яким іде переведення рядка, а не лише переведенням рядка. Ви не вводитимете ці символи вручну, але побачите їх у захопленні пакетів.
Щоб переглянути реальну пару запиту й відповіді, виконайте цю команду для сайту, яким ви керуєте:
curl -sS -o /dev/null -D - https://example.com/-D - виводить заголовки відповіді в термінал, а -o /dev/null відкидає тіло. Використовуйте цей варіант замість curl -I, оскільки -I надсилає запит HEAD. Сервер застосунків, який обробляє HEAD інакше, ніж GET, а так роблять багато серверів, покаже вам заголовки, які браузер ніколи не отримує. curl -v виводить обидві сторони, позначаючи рядки запиту як >, а рядки відповіді — як <.
Як виглядає рядок запиту в журналі доступу 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-з’єднання (протокол керування передаванням). За проксі тут буде адреса проксі, а не відвідувача.- Перше
-— фіксований заповнювач. Друге — це$remote_user, яке заповнюється лише за використання HTTP Basic Authentication. "GET /pricing HTTP/1.1"— це$request, рядок запиту, скопійований точно в тому вигляді, у якому він надійшов.200— статус, який повернув ваш сервер, а не статус, який побачив відвідувач.5310— це$body_bytes_sent, лише тіло відповіді. Заголовки відповіді не враховуються, тому це значення завжди менше за фактичну кількість переданих байтів.- Останні два поля в лапках — це
RefererіUser-Agent. Обидва надходять від клієнта, тому можуть містити будь-які дані.
Оскільки $request копіюється без змін, некоректні дані також з’являються без змін. Клієнт, який встановлює TLS-з’єднання (захист на транспортному рівні) із незашифрованим портом 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 (тегом сутності), який ви все ще розпізнаєте, тому ви відповіли заголовками без тіла. Велика кількість 304s у журналі означає, що кешування працює.
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 не може прочитати файл. Найчастіше батьківський каталог не має дозволу на виконання для інших користувачів. directory index of "/srv/site/" is forbidden означає, що шлях вказує на каталог без index-файлу, коли 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. Якщо після додавання нового virtual host повертається неправильний сайт, майже завжди причина саме в цьому: ім’я не збіглося, тому запит перейшов до сервера за замовчуванням. Перевірте це без зміни 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 слід використовувати для всього, що залежить від конкретного користувача, оскільки shared cache, який зберігає сторінку після входу, може віддати її наступній особі, що запитає той самий URL. private — проміжний варіант: браузер може зберігати відповідь, а shared cache — ні.
X-Forwarded-For потрібен тому, що proxy приховує адресу відвідувача. Після проходження запиту через reverse proxy $remote_addr містить адресу proxy. Тому журнали, геолокація та обмеження частоти запитів бачать лише одного клієнта. Proxy має передати початкову адресу:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;Потім receiving server потрібно налаштувати так, щоб він довіряв цьому значенню, і чітко вказати, кому саме він може довіряти:
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 запобігає конкретній і дуже поширеній помилці. Proxy завершує TLS і передає запит застосунку через звичайний HTTP. Застосунок бачить звичайний запит, вирішує, що відвідувач має перейти на HTTPS, і відповідає 301 https://example.com/. Браузер виконує редирект, proxy знову завершує 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, а не накладається поверх нього, тому новому з’єднанню потрібно менше мережевих обмінів. З цього випливають два практичні наслідки: UDP-порт 443 має бути відкритий у кожному firewall на маршруті, а будь-яка мережа, яка обмежує або блокує 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 з’явився в mainline 1.25.0. Для сайту HTTP/3 потрібен listen 443 quic reuseport; разом зі звичайним listen 443 ssl;.
Проксі відрізняються за рівнем підтримки цього протоколу, тому це варто перевірити для версії, яку ви фактично використовуєте. Станом на August 2026 Caddy обслуговує HTTP/3 за замовчуванням без додаткової конфігурації. nginx потребує явного слухача quic і заголовка Alt-Svc, описаного вище. Traefik вмикає HTTP/3 для кожної точки входу через явний параметр http3. Якщо ви завершуєте TLS у Traefik перед кількома Docker-застосунками, версія протоколу, яку отримують відвідувачі, визначається саме там, а з’єднання від проксі до контейнера зазвичай працює через звичайний HTTP/1.1 незалежно від протоколу, який узгодив браузер.
Перевіряйте, а не припускайте. curl --http3 -sS -o /dev/null -D - https://example.com/ працює лише тоді, коли curl -V містить HTTP3 серед своїх можливостей, а більшість збірок дистрибутивів цього не включає. Надійний спосіб перевірки — власний журнал: додайте $server_protocol до формату й перегляньте, який протокол узгоджують реальні браузери. Перед цим переконайтеся, що UDP 443 справді відкритий, оскільки firewall, який дозволяє лише 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 або content delivery network цією адресою є адреса проксі. Адреса відвідувача надходить у заголовку 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 означає, що користувач worker-процесу nginx не може прочитати файл. Найчастіше причина полягає у відсутності дозволу на виконання для інших користувачів у батьківського каталогу, а не в неправильному режимі самого файлу. directory index of ... is forbidden означає, що запит відповідає каталогу без index-файлу, коли autoindex вимкнено. Явне правило deny у відповідному блоці location також повертає 403. Тому перегляньте цей блок, якщо в журналі помилок немає пояснення.