Skip to main content

Як працює модуль HTTP у Node.js?

Модуль 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

МодульПротоколКоли використовувати
httpHTTP/1.1Локальна розробка, внутрішні сервіси в приватній мережі
httpsHTTPS + TLSБудь-який продакшн-сервер для реальних користувачів
http2HTTP/2Коли потрібен multiplexing або server push

https потребує TLS-сертифікати. http2 використовує інший API (http2.createSecureServer) і підтримує мультиплексовані потоки через одне TCP-з'єднання.

http vs Express

ФункціяСировинний httpExpress.js
МаршрутизаціяРучна if/else по req.urlapp.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-реверс-проксі.

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

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

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

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