HTTP-клиент в Node.js: как делать правильно
fetch, undici, axios и got решают одну задачу по-разному. Разбираем, чем платишь за каждый подход, где утекают сокеты и почему запрос без таймаута однажды подвесит сервис.

Запрос к внешнему API в Node.js можно написать пятью способами, и четыре из них однажды подведут в проде. Классика: сервис работает месяцами, потом внешний API начинает отвечать за 40 секунд вместо 40 миллисекунд, event loop забивается висящими запросами, а мониторинг показывает рост памяти и открытых сокетов. Разбираемся, какой клиент выбрать в Node.js 20+ и что именно настроить, чтобы этого не случилось.
Речь про исходящие HTTP-запросы из серверного кода: обращения к платёжкам, к сервисам поставщиков, к своим же микросервисам. Разбираем четыре варианта — встроенный fetch, undici, axios и got — с ценой каждого. Ретраи и circuit breaker упоминаем, но подробно не разворачиваем: это отдельная тема.
Что изменилось: fetch теперь встроенный
Начиная с Node.js 18 глобальный fetch доступен без флагов, в 21 он перестал печатать предупреждение об экспериментальности. Под капотом это undici — HTTP-клиент, который команда Node пишет как замену старому модулю http для исходящих запросов. То есть fetch, undici.request и undici.fetch — это один движок с разными фасадами.
Практический вывод: тянуть axios в новый проект только ради того, чтобы сделать GET, больше незачем. Но у axios и got остаются причины для жизни, и дело не в возрасте кода.
Главная ловушка встроенного fetch
У fetch в Node нет таймаута по умолчанию. Ни на соединение, ни на получение ответа. Запрос к зависшему серверу будет ждать столько, сколько живёт сокет. Это не баг, это соответствие спецификации браузерного fetch — но на сервере поведение опасное.
// Node.js 20+, встроенный fetch
// AbortSignal.timeout прерывает запрос через заданное число мс
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000), // 5 секунд суммарно
});
if (!response.ok) {
// fetch НЕ бросает ошибку на 4xx/5xx — статус проверяем сами
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
Два неочевидных момента в этом фрагменте. Первый: AbortSignal.timeout ограничивает всё время до конца получения тела, а не только установку соединения. Второй: fetch считает успехом любой ответ, который дошёл, включая 500. Забыли проверить response.ok — и дальше по коду поедет тело страницы ошибки, распарсенное как ваши данные.
Четыре клиента и цена каждого
Встроенный fetch
Ноль зависимостей, знакомый по браузеру API, промисы из коробки. Платите за это отсутствием таймаута по умолчанию, ручной проверкой статуса и тем, что стриминг тела через ReadableStream непривычен тем, кто пришёл из Node-стримов.
undici напрямую
undici.request — низкоуровневый доступ к тому же движку. Он быстрее fetch на горячем пути, потому что не строит объекты Request/Response по браузерной спецификации. Даёт прямой контроль над пулом соединений через Agent и Pool. Цена — API менее привычный, тело возвращается как поток с методами .json(), .text(), а ошибки статуса снова не бросаются сами.
axios
Interceptors для запросов и ответов, автоматический разбор JSON, встроенный timeout, единый объект ошибки. Бросает исключение на 4xx/5xx по умолчанию — многим это удобнее. Платите весом зависимости и тем, что под капотом до недавнего времени лежал старый Node-модуль http, а не undici, то есть вы теряете часть выигрыша по производительности.
got
Богатые ретраи из коробки, хуки, поддержка потоков в Node-стиле, продуманная работа с редиректами и таймаутами по фазам (на DNS, на соединение, на ответ). Ориентирован именно на сервер, а не на браузер. Цена — ещё одна зависимость и ESM-only в свежих мажорных версиях, что ломает сборки на чистом CommonJS без настройки.
| Критерий | fetch | undici | axios | got |
|---|---|---|---|---|
| Зависимость | нет | 1 пакет (или встроен) | 1 пакет | 1 пакет |
| Таймаут по умолчанию | нет | нет | нет, но задаётся полем | есть по фазам |
| Бросает на 4xx/5xx | нет | нет | да | да |
| Ретраи из коробки | нет | нет | нет | да |
| Движок | undici | undici | node http | node http |
Клиент не решает проблему надёжности сам по себе. Решают таймаут, лимит на пул соединений и обработка неуспешных статусов. Библиотека лишь определяет, насколько удобно это всё включить.
Что ломается первым: сокеты и keep-alive
Самая частая беда на проде — не выбор библиотеки, а неограниченный пул соединений. По умолчанию undici (а значит и fetch) держит keep-alive соединения к каждому хосту. Это хорошо для скорости, но если хост один и трафик высокий, число одновременных соединений и pipelining нужно контролировать вручную.
import { Agent, setGlobalDispatcher } from 'undici';
// Ограничиваем пул: не больше 128 соединений на origin,
// keep-alive держим до 60 секунд простоя
const agent = new Agent({
connections: 128,
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 600_000,
});
// Применяем ко всем последующим вызовам fetch/undici
setGlobalDispatcher(agent);
Точные имена и допустимые значения опций Agent сверяйте с документацией undici для вашей версии — набор параметров между мажорными релизами менялся. Проверить, что pool работает, проще всего по метрике открытых TCP-соединений к целевому хосту: при keep-alive она должна держаться на плато, а не расти линейно с числом запросов.
Утечка на неразобранном теле
Ещё одна ловушка undici и fetch: если вы получили ответ, но не прочитали его тело до конца, соединение может не вернуться в пул. При работе через fetch всегда доводите тело до конца — вызовом .json(), .text() либо, если тело не нужно, явно отмените его через response.body?.cancel().
Когда что выбирать
- fetch — новый сервис, простые запросы, важна нулевая зависимость. Обязательно добавьте
AbortSignal.timeoutи проверкуresponse.okв каждый вызов или в обёртку. - undici напрямую — горячий путь с тысячами запросов в секунду, нужен контроль пула и максимальная скорость. Готовы писать чуть более низкоуровневый код.
- axios — уже используется в проекте, нужны interceptors для единой авторизации и логирования, а команде привычнее «ошибка бросается сама».
- got — интеграции с капризными внешними API, где нужны ретраи с backoff, тонкие таймауты по фазам и работа с потоками загрузки-выгрузки.
Обёртка, которую стоит написать один раз
Независимо от клиента, размазывать таймаут и проверку статуса по всему коду — плохо. Соберите тонкую обёртку и вызывайте только её.
// Обёртка над встроенным fetch, Node.js 20+
async function httpJson(url, options = {}) {
const { timeoutMs = 5000, ...rest } = options;
const response = await fetch(url, {
...rest,
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
// читаем тело, чтобы вернуть соединение в пул
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
}
return response.json();
}
// Использование
const user = await httpJson('https://api.example.com/user/42', {
timeoutMs: 3000,
headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
Такая обёртка закрывает три самые частые ошибки сразу: висящий запрос, необработанный статус ошибки и утечку сокета на теле неуспешного ответа.
Что настроить дальше
Клиент выбран, таймаут и пул на месте — дальше смотрите на устойчивость к деградации внешних сервисов. Следующие шаги: ретраи только на идемпотентных запросах и только на сетевые ошибки и 5xx (на 4xx повторять бессмысленно); circuit breaker, чтобы не долбить упавший сервис; метрики по времени ответа с разбивкой на фазы (DNS, соединение, первый байт). В проде мерьте p99 времени внешних вызовов и число открытых сокетов к каждому хосту — эти две метрики ловят большинство проблем раньше, чем они дойдут до пользователя.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: undici — HTTP/1.1 client for Node.js (GitHub), Node.js documentation: Globals — fetch
Частые вопросы
У меня Node.js 16, там нет встроенного fetch. Что делать?
Почему мой fetch не бросает ошибку при статусе 500?
Запросы иногда зависают навсегда — в чём причина?
Стоит ли выбрасывать axios из старого проекта ради fetch?
Как проверить, что пул соединений настроен правильно?
Ретраи лучше писать самому или брать got?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.