Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює модуль HTTP у Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Модуль **`http`** є вбудованим у Node.js і дозволяє створювати HTTP-сервери та виконувати вихідні запити без встановлення залежностей. `http.createServer()` приймає callback з `req` (читабельний потік) і `res` (записуваний потік). ```js const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); }); server.listen(3000); ``` **Головне:** `req` і `res` є потоками. Тіло запиту доступне не одразу: збираєш його з подій `data`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)ЗображенняМодуль **`http`** є вбудованим у Node.js і дозволяє створювати HTTP-сервери та виконувати вихідні HTTP-запити без жодних npm-залежностей. ## Теорія ### TL;DR - Кожен HTTP-сервер у Node запускається в одному процесі, але обробляє багато з'єднань через event loop - `req` є `IncomingMessage` (читабельний потік), `res` є `ServerResponse` (записуваний потік) - Тіло запиту надходить не одразу: збираєш його з подій `data` вручну - `http.createServer()` під капотом повертає `net.Server`. Express огортає ту саму функцію - `https` беремо для продакшну (TLS), `http2` коли потрібен multiplexing ### Швидкий приклад ```js const http = require('http'); const server = http.createServer((req, res) => { // req = IncomingMessage (читабельний потік) // res = ServerResponse (записуваний потік) res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'Hello' })); }); server.listen(3000, () => console.log('Сервер на порту 3000')); ``` Callback спрацьовує на кожний вхідний запит. `res.end()` відправляє відповідь і закриває з'єднання. ### Як працює цикл запит-відповідь Коли клієнт підключається, TCP-шар Node приймає з'єднання і починає парсити HTTP-повідомлення. Після того як заголовки розібрані, Node викликає твій callback з `req` і `res`. Тіло запиту в цей момент ще не прочитане: воно продовжує надходити через потік. Тому події `data` і `end` потрібно слухати вручну. `res.writeHead()` записує рядок статусу і заголовки. `res.end()` відправляє тіло і закриває відповідь. Про відсутній `res.end()` найкраще дізнаєшся один раз, коли curl просто зависає і ти десять хвилин дивишся в консоль в пошуках проблеми. ### Читання об'єкта запиту ```js const server = http.createServer((req, res) => { console.log(req.method); // 'GET', 'POST', ... console.log(req.url); // '/api/users?page=2' console.log(req.headers); // { host: 'localhost:3000', ... } const url = new URL(req.url, `http://${req.headers.host}`); console.log(url.pathname); // '/api/users' console.log(url.searchParams.get('page')); // '2' res.end('ok'); }); ``` `req.url` є сирим рядком. Для парсингу query-параметрів використовуй вбудований конструктор `URL`. Розбиття рядка ламається одразу, як тільки з'являється query string. ### Збирання тіла POST-запиту Тіло надходить частинами. Збираєш їх сам: ```js const server = http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/api/users') { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { const user = JSON.parse(body); res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ created: user })); }); } }); ``` Тут немає ліміту розміру за замовчуванням. Клієнт може надсилати гігабайти. У продакшні завжди обмежуй розмір тіла. Express робить це автоматично через `bodyParser`. ### Вихідні HTTP-запити ```js const http = require('http'); // GET http.get('http://api.example.com/data', (res) => { let data = ''; res.on('data', (chunk) => (data += chunk)); res.on('end', () => console.log(JSON.parse(data))); }); // POST const payload = JSON.stringify({ name: 'Alice' }); const req = http.request( { hostname: 'api.example.com', port: 80, path: '/users', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), }, }, (res) => { let data = ''; res.on('data', (chunk) => (data += chunk)); res.on('end', () => console.log(JSON.parse(data))); } ); req.write(payload); req.end(); ``` Callback у `http.request()` є скороченням для події `response`. Без `req.end()` запит не відправиться. ### http vs https vs http2 | Модуль | Протокол | Коли використовувати | |--------|----------|----------------------| | `http` | HTTP/1.1 | Локальна розробка, внутрішні сервіси в приватній мережі | | `https` | HTTPS + TLS | Будь-який продакшн-сервер для реальних користувачів | | `http2` | HTTP/2 | Коли потрібен multiplexing або server push | `https` потребує TLS-сертифікати. `http2` використовує інший API (`http2.createSecureServer`) і підтримує мультиплексовані потоки через одне TCP-з'єднання. ### http vs Express | Функція | Сировинний `http` | Express.js | |---------|--------------------|------------| | Маршрутизація | Ручна `if/else` по `req.url` | `app.get('/path', handler)` | | Парсинг тіла | Ручне збирання потоку | `express.json()` middleware | | Middleware | Відсутнє | Підтримується з коробки | | Статичні файли | Вручну | `express.static()` | Express викликає `http.createServer()` всередині. Коли пишеш `app.listen(3000)`, Express запускає `http.createServer(app).listen(3000)` за лаштунками. Ти завжди використовуєш `http`. Express просто дає кращий API зверху. ### Типові помилки **1. Забутий `res.end()`** ```js // Зламано - клієнт зависає нескінченно http.createServer((req, res) => { res.writeHead(200); // res.end() відсутній }); ``` Потік відповіді ніколи не закривається. Клієнт чекає до таймауту. **2. Запис заголовків після початку тіла** ```js // Зламано http.createServer((req, res) => { res.write('some data'); res.writeHead(200); // Error: Cannot set headers after they are sent }); ``` `res.writeHead()` має стояти перед будь-яким `res.write()`. **3. Немає обробника помилок на `req`** ```js // Тендітно - процес впаде при обриві з'єднання req.on('data', (chunk) => { body += chunk; }); // Правильно req.on('data', (chunk) => { body += chunk; }); req.on('error', (err) => { res.writeHead(400); res.end(err.message); }); ``` Якщо клієнт обриває з'єднання посередині запиту, Node генерує подію `error` на `req`. Без обробника процес впаде. **4. Немає ліміту розміру тіла** ```js let body = ''; req.on('data', (chunk) => { body += chunk; if (body.length > 1e6) { // ліміт 1MB req.destroy(); res.writeHead(413); res.end('Payload too large'); } }); ``` **5. Парсинг `req.url` через розбиття рядка** ```js // Ламається при query string типу /users/42?format=json const id = req.url.split('/')[2]; // Надійно const url = new URL(req.url, `http://${req.headers.host}`); const id = url.pathname.split('/')[2]; ``` ### Де зустрічається в продакшні - Express і Fastify обидва викликають `http.createServer()` під капотом - Next.js custom servers використовують його напряму - Легковагові Node-мікросервіси без повноцінного фреймворку - `http.get()` і `http.request()` часто зустрічаються в старих кодових базах, написаних до того як `fetch` з'явився в Node 18 ### Питання на співбесіді **Q:** Чому Express використовує `http.createServer()` замість власного TCP-шару? **A:** Тому що `http` є вбудованим TCP-парсером і обробником HTTP-повідомлень у Node. Кожен Node HTTP-фреймворк проходить через нього. Express додає маршрутизацію, ланцюжки middleware і зручніші хелпери поверх того самого callback. **Q:** Що таке `IncomingMessage` і чому він розширює readable stream? **A:** `IncomingMessage` представляє вхідне HTTP-повідомлення. Він розширює `stream.Readable`, бо тіло може бути довільно великим. Читання через потік дозволяє не завантажувати весь payload у пам'ять одразу. **Q:** Як Node обробляє паралельні запити, якщо він однопоточний? **A:** Event loop обробляє I/O асинхронно. Поки один запит чекає відповіді від бази даних, loop підхоплює інші вхідні з'єднання. Потоки для I/O-bound задач не потрібні. **Q:** Що станеться, якщо викликати `res.end()` двічі? **A:** Node кине `Error: write after end`. Другий виклик намагається записати в уже закритий потік. **Q:** Як захистити `http`-сервер від клієнтів що надсилають великі тіла? **A:** Виставити ліміт розміру всередині обробника `data` і викликати `req.destroy()` при перевищенні порогу. Також варто виставити `server.timeout`, щоб автоматично закривати неактивні з'єднання. ## Приклади ### Простий JSON API ```js const http = require('http'); const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; const server = http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/api/users') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(users)); return; } res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); }); server.listen(3000); ``` GET на `/api/users` повертає список. Все інше отримує 404. Той самий патерн, що використовують роути Express, тільки без шару абстракції. ### Створення користувача через POST ```js const http = require('http'); const server = http.createServer((req, res) => { if (req.method !== 'POST' || req.url !== '/api/users') { res.writeHead(404); res.end(); return; } let body = ''; req.on('data', (chunk) => { body += chunk.toString(); if (body.length > 1e5) { // захист 100KB req.destroy(); } }); req.on('end', () => { try { const user = JSON.parse(body); res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id: Date.now(), ...user })); } catch { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid JSON' })); } }); req.on('error', () => { res.writeHead(400); res.end(); }); }); server.listen(3000); ``` Повний продакшн-патерн: захист від великого тіла, обробка помилки парсингу JSON і слухач помилок на потоці запиту. ### Проксування запиту до внутрішнього сервісу ```js const http = require('http'); const server = http.createServer((req, res) => { const proxy = http.request( { hostname: 'internal-service', port: 8080, path: req.url, method: req.method, headers: req.headers, }, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); // стрімимо відповідь назад клієнту } ); req.pipe(proxy); // стрімимо тіло до внутрішнього сервісу proxy.on('error', () => { res.writeHead(502); res.end('Bad gateway'); }); }); server.listen(3000); ``` Pipe `req` напряму у proxy-запит дозволяє уникнути буферизації тіла в пам'яті. Відповідь повертається так само. Це концептуальна основа HTTP-реверс-проксі.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.