Як працює HTTP та з чого складається HTTP-запит
HTTP (HyperText Transfer Protocol) — це протокол прикладного рівня без стану (stateless), де клієнт надсилає структурований текстовий запит на сервер, а сервер відповідає повідомленням зі статус-кодом, заголовками та необов'язковим тілом.
Теорія
TL;DR
- HTTP схожий на листування: пишеш метод + шлях + заголовки + тіло, відправляєш через TCP, отримуєш відповідь зі статус-кодом
- Запит складається з п'яти частин: метод, URL, версія HTTP, заголовки, тіло
- Stateless означає, що сервер забуває попередній запит одразу після відповіді
- GET для читання, POST для створення, PUT/PATCH для оновлення, DELETE для видалення
- HTTP/2 використовуй коли треба багато паралельних запитів (мультиплексує потоки в одному TCP-з'єднанні)
Швидкий приклад
// Ось що відбувається коли викликаєш 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-запиту.
// Неправильно: тіло ігнорується більшістю серверів і проксі
fetch('/search', { method: 'GET', body: 'q=shoes' });
// Правильно: використовуй параметри запиту
fetch('/search?q=shoes');Розраховувати на те, що сервер тебе пам'ятає. HTTP stateless. Кожен запит — чистий аркуш. Перший запит додає товар у кошик; другий запит приходить і сервер не знає, хто ти взагалі.
// Неправильно: без 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.
// Неправильно: сервер отримує тіло як сирий потік, 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
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 на практиці
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.