Skip to main content

Як працює HTTP та з чого складається HTTP-запит

HTTP (HyperText Transfer Protocol) — це протокол прикладного рівня без стану (stateless), де клієнт надсилає структурований текстовий запит на сервер, а сервер відповідає повідомленням зі статус-кодом, заголовками та необов'язковим тілом.

Теорія

TL;DR

  • HTTP схожий на листування: пишеш метод + шлях + заголовки + тіло, відправляєш через TCP, отримуєш відповідь зі статус-кодом
  • Запит складається з п'яти частин: метод, URL, версія HTTP, заголовки, тіло
  • Stateless означає, що сервер забуває попередній запит одразу після відповіді
  • GET для читання, POST для створення, PUT/PATCH для оновлення, DELETE для видалення
  • HTTP/2 використовуй коли треба багато паралельних запитів (мультиплексує потоки в одному TCP-з'єднанні)

Швидкий приклад

javascript
// Ось що відбувається коли викликаєш fetch() fetch('https://jsonplaceholder.typicode.com/users/1') .then(res => { console.log('Status:', res.status); // 200 console.log('Content-Type:', res.headers.get('content-type')); // application/json return res.json(); }) .then(data => console.log('Name:', data.name)); // Leanne Graham // Браузер надсилає ось це: // GET /users/1 HTTP/1.1 // Host: jsonplaceholder.typicode.com // Accept: application/json

Коли ти викликаєш fetch(), браузер серіалізує ці п'ять частин у байти, відкриває TCP-з'єднання на порт 443 (HTTPS) і записує їх у сокет. Сервер зчитує потік, парсить і відправляє відповідь назад.

Як виглядає HTTP-запит насправді

HTTP/1.1-запит — це звичайний текст. Ось сирий POST-запит:

POST /todos HTTP/1.1 Host: api.example.com Content-Type: application/json Authorization: Bearer eyJhbGc... Content-Length: 32 {"text": "Buy milk", "done": false}

Чотири секції, між заголовками і тілом — порожній рядок. Ось і весь формат.

Метод вказує серверу, що робити з ресурсом. GET читає, POST створює, PUT замінює, PATCH оновлює частково, DELETE видаляє. Метод також впливає на кешування: GET-відповіді кешуються, POST — ні.

URL — адреса ресурсу. Містить шлях (/todos) і, за потреби, параметри запиту (?page=1&sort=asc).

Заголовки — це метадані у форматі ключ-значення. Content-Type: application/json каже серверу, як парсити тіло. Authorization: Bearer token аутентифікує запит. Host обов'язковий в HTTP/1.1, бо один IP може обслуговувати багато доменів.

Тіло містить корисне навантаження. Використовується лише в POST, PUT і PATCH. GET і DELETE не мають тіла (детальніше в розділі «Типові помилки»).

Як запит подорожує мережею

Браузер знаходить IP-адресу домену через DNS, відкриває TCP-з'єднання на порт 80 (HTTP) або 443 (HTTPS) і записує байти запиту в буфер ядра. Для HTTPS спочатку відбувається TLS-рукостискання (handshake): клієнт надсилає привітання, сервер повертає сертифікат, вони домовляються про шифр — і далі всі байти шифруються. Структура самого запиту всередині не змінюється.

На стороні сервера бібліотека http-parser (C, використовується в Node.js і nginx) зчитує вхідний потік і заповнює метод, шлях, заголовки та тіло. Далі працює твій код застосунку.

HTTP/1.1 проти HTTP/2

HTTP/1.1 — текстовий протокол. На одному TCP-з'єднанні обробляється один запит за раз. Якщо відправляєш запит A, а потім запит B на тому ж з'єднанні, B чекає поки A завершиться. Браузери обходили це, відкриваючи шість з'єднань на домен, але це все одно неефективно.

HTTP/2 замінює текстовий формат бінарними фреймами і мультиплексує кілька потоків запитів через одне TCP-з'єднання. Повільний запит не блокує інші. Ті ж самі методи і заголовки, але інший формат передачі.

HTTP/3 (QUIC) іде далі: відмовляється від TCP на користь UDP з відновленням втрат на рівні потоку. Втрачений пакет зупиняє лише свій потік, а не все з'єднання.

Типові помилки

Надсилати тіло з GET-запитом. RFC 7231 формально не забороняє це, але проксі-сервери і кеші можуть проігнорувати або відкинути тіло. На сервері отримаєш нуль байт і збиті логи. На продакшені це зазвичай виявляє CDN: origin-сервер обробляє запит нормально, але після додавання CloudFront або Cloudflare кешований результат починає повертати неправильні дані — і ніхто не пов'язує це з тілом GET-запиту.

javascript
// Неправильно: тіло ігнорується більшістю серверів і проксі fetch('/search', { method: 'GET', body: 'q=shoes' }); // Правильно: використовуй параметри запиту fetch('/search?q=shoes');

Розраховувати на те, що сервер тебе пам'ятає. HTTP stateless. Кожен запит — чистий аркуш. Перший запит додає товар у кошик; другий запит приходить і сервер не знає, хто ти взагалі.

javascript
// Неправильно: без session-middleware req.session не існує app.post('/cart', (req, res) => { req.session.cart.push(req.body.item); // TypeError: Cannot read property 'cart' of undefined }); // Правильно: спочатку підключи session-middleware app.use(session({ secret: 'key', resave: false, saveUninitialized: true }));

Не вказувати Content-Type у POST. Якщо надсилаєш JSON-тіло без заголовка Content-Type: application/json, Express (і більшість фреймворків) не зрозуміє, як його парсити. req.body буде undefined.

javascript
// Неправильно: сервер отримує тіло як сирий потік, req.body = undefined fetch('/todos', { method: 'POST', body: JSON.stringify({ text: 'Buy milk' }) // Без заголовка Content-Type }); // Правильно fetch('/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Buy milk' }) });

Вимикати перевірку сертифіката в Node.js. rejectUnauthorized: false здається нешкідливим у розробці, але ця звичка поширюється на продакшн. Неправильно налаштований сервер перетворюється на атаку типу «людина посередині» (man-in-the-middle).

Де зустрічається

  • React: useEffect + fetch('/api/users') надсилає GET і парсить JSON-відповідь у стан компонента
  • Express: app.get('/profile', (req, res) => res.json(user)) відповідає з 200 + Content-Type: application/json
  • Next.js API routes: export default (req, res) => res.status(201).json(data) парсить повний запит включно з методом і заголовками
  • Apollo Client: надсилає GraphQL-запити як POST із заголовком Authorization і JSON-тілом
  • AWS Lambda + API Gateway: отримує метод, шлях, заголовки і тіло як розпарсений об'єкт події

Питання на співбесіді

Q: Яка різниця між HTTP/1.1 і HTTP/2?
A: HTTP/1.1 текстовий і обробляє один запит на TCP-з'єднання за раз, що спричиняє блокування голови черги (head-of-line blocking). HTTP/2 використовує бінарні фрейми і мультиплексує кілька потоків в одному з'єднанні, тому повільний запит не зупиняє інші.

Q: Що знаходиться в рядку запиту, а що в заголовках?
A: Рядок запиту — це перший рядок: METHOD /path HTTP/1.1. Заголовки йдуть після, по одному в рядку у форматі Name: Value, і закінчуються порожнім рядком перед тілом.

Q: Як HTTPS змінює запит?
A: Він обгортає запит у TLS. Після рукостискання всі байти, включно із заголовками і тілом, передаються в зашифрованому вигляді. HTTP-формат всередині не змінюється.

Q: Що означають статус-коди 200, 201 і 204?
A: 200 OK з тілом відповіді. 201 Created — зазвичай після POST, з заголовком Location що вказує на новий ресурс. 204 No Content — після успішного DELETE.

Q: Що викликає CORS preflight-запит?
A: Браузер надсилає OPTIONS-запит, коли реальний запит є крос-доменним і використовує нестандартний метод або заголовок, наприклад Authorization. Сервер має відповісти Access-Control-Allow-Origin перш ніж браузер надішле справжній запит.

Q: Як HTTP/3 (QUIC) вирішує проблему head-of-line blocking, яка є навіть у HTTP/2?
A: HTTP/2 мультиплексує потоки через одне TCP-з'єднання, але TCP сам блокує всі потоки якщо втрачено один пакет. QUIC працює через UDP і відновлює втрати на рівні окремого потоку, тому втрачений пакет зупиняє лише свій потік.

Приклади

Базовий: зчитування частин запиту в Express

javascript
const express = require('express'); const app = express(); app.use(express.json()); // парсить тіло коли Content-Type: application/json app.post('/todos', (req, res) => { console.log('Method:', req.method); // POST console.log('Path:', req.path); // /todos console.log('Auth:', req.headers.authorization); // Bearer xyz console.log('Body:', req.body); // { text: 'Buy milk', done: false } res.status(201).json({ id: 1, ...req.body }); // Відповідь: HTTP/1.1 201 Created // Content-Type: application/json // { "id": 1, "text": "Buy milk", "done": false } }); app.listen(3000); // Тест: // curl -X POST http://localhost:3000/todos \ // -H "Content-Type: application/json" \ // -H "Authorization: Bearer xyz" \ // -d '{"text":"Buy milk","done":false}'

Express розбирає всі п'ять частин запиту. Middleware express.json() зчитує потік тіла, перевіряє Content-Type і парсить JSON у req.body. Без нього req.body буде undefined.

Проміжний: head-of-line blocking на практиці

javascript
const https = require('https'); // Два запити на окремих з'єднаннях - обидва стартують одразу const req1 = https.request('https://httpbin.org/get', { method: 'GET' }, res1 => { console.log('Req1 status:', res1.statusCode); // 200, приходить швидко }); const req2 = https.request('https://httpbin.org/delay/2', { method: 'GET' }, res2 => { console.log('Req2 status:', res2.statusCode); // 200, приходить через 2с }); req1.end(); req2.end(); // Node відкриває окремі TCP-з'єднання, тому вони йдуть паралельно // При HTTP/1.1 на ОДНОМУ з'єднанні req2 заблокував би req1 на ці 2 секунди // Саме тому браузери відкривали 6 з'єднань на домен в епоху HTTP/1.1

Кожен виклик https.request() відкриває своє TCP-з'єднання, тому обидва йдуть паралельно. На одному HTTP/1.1-з'єднанні повільний запит зупинив би все позаду нього. HTTP/2 вирішує це, передаючи обидва запити в одному з'єднанні без блокування.

Просунутий: інспекція сирої структури запиту через Node http

javascript
const http = require('http'); http.createServer((req, res) => { // req.method, req.url, req.httpVersion парсяться з рядка запиту console.log(`${req.method} ${req.url} HTTP/${req.httpVersion}`); // GET /api/data HTTP/1.1 // req.headers - об'єкт з усіма парами ключ-значення (ключі в нижньому регістрі) console.log('Host:', req.headers.host); console.log('Accept:', req.headers.accept); console.log('User-Agent:', req.headers['user-agent']); // Тіло треба читати як потік, не як рядок let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { console.log('Body:', body); // сирий рядок, парс через JSON.parse() якщо треба res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); }); }).listen(3000);

Модуль http в Node.js показує рівно п'ять частин запиту напряму. Тіло — це потік, не рядок. Саме тому існує express.json(): він обробляє стрімінг і парсинг, щоб ти одразу отримав req.body замість ручного зв'язування подій data і end.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?