Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює HTTP та з чого складається HTTP-запит». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**HTTP** — це протокол запит-відповідь без стану (stateless) для передачі даних між клієнтом і сервером. HTTP-запит (HTTP request) складається з п'яти частин: метод, URL, версія HTTP, заголовки, тіло. ``` POST /api/todos HTTP/1.1 Host: example.com Content-Type: application/json Authorization: Bearer token {"text": "Buy milk"} ``` **Ключове:** сервер не пам'ятає попередні запити. Кожен запит має нести весь потрібний контекст у власних заголовках або тілі.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.