← Назад к блогу

Undici ProxyAgent в Node.js: настройка HTTP/SOCKS5-прокси и ротация IP без лишних зависимостей

Разбираем, как использовать встроенный undici ProxyAgent в Node.js для работы с HTTP и SOCKS5-прокси, организовать ротацию IP и обойтись без https-proxy-agent и socks-proxy-agent.

📅18 октября 2026 г.

Большинство гайдов по прокси в Node.js советуют тащить в проект https-proxy-agent, socks-proxy-agent и ещё пару пакетов сверху. На деле с версии Node.js 18+ встроенный HTTP-клиент undici уже содержит ProxyAgent, который закрывает большинство задач парсинга и тестирования API через прокси. В этой статье разберём, как настроить HTTP/HTTPS-туннелирование через undici, организовать ротацию IP между запросами и что делать, если нужен SOCKS5, а тащить дополнительную зависимость не хочется.

Зачем undici вместо axios и request

undici — это HTTP/1.1 и HTTP/2 клиент, написанный командой Node.js и начиная с 18 версии лежащий в основе глобального fetch. У него нет транзитивных зависимостей, он быстрее axios в большинстве бенчмарков на connection pooling, и что важно для нашей темы — у него есть нативный модуль для работы с прокси через HTTP CONNECT туннелирование.

Когда вы парсите Wildberries, Ozon или тестируете рекламные креативы из разных гео, каждый лишний пакет в node_modules — это потенциальная точка отказа при обновлении Node.js. undici поставляется вместе с рантаймом, поэтому вы получаете актуальную реализацию TLS, HTTP/2 и keep-alive без риска, что абандонная библиотека сломается на новой мажорной версии.

Установка максимально простая:

npm install undici

Если у вас Node.js 20+, часть API доступна глобально без импорта, но для ProxyAgent импорт всё равно нужен, так как он не экспортируется в глобальный fetch по умолчанию.

Базовая настройка ProxyAgent

Минимальный рабочий пример запроса через прокси выглядит так:

import { ProxyAgent, fetch } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

const data = await response.json();
console.log(data);

Ключевой момент — параметр dispatcher. Это не опция agent, как в старом модуле http, а именно диспетчер undici, который перехватывает соединение ещё до TLS-рукопожатия и устанавливает туннель через CONNECT-метод к целевому хосту. Для HTTPS-запросов через прокси это критично: обычный HTTP-прокси не видит содержимое зашифрованного трафика, он просто пробрасывает байты внутри TCP-туннеля.

Если вы работаете не с fetch, а с низкоуровневым undici.request, синтаксис аналогичный:

import { request, ProxyAgent } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const { statusCode, body } = await request('https://api.example.com/data', {
  dispatcher: proxyAgent,
});

console.log(statusCode);
for await (const chunk of body) {
  console.log(chunk.toString());
}

Для продакшн-парсеров, которые делают сотни запросов в минуту, важно не создавать новый ProxyAgent на каждый запрос — это убивает connection pooling. Создавайте инстанс агента один раз на пул соединений и переиспользуйте его между вызовами.

Аутентификация прокси по логину и паролю

Большинство платных прокси-провайдеров выдают доступ по паре логин/пароль, а не по IP-whitelisting. В undici учётные данные передаются прямо в URL прокси:

import { ProxyAgent, fetch } from 'undici';

const login = 'user123';
const password = 'pass456';
const proxyUrl = `http://${login}:${password}@gate.proxycove.com:9000`;

const proxyAgent = new ProxyAgent(proxyUrl);

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

console.log(await response.json());

Обратите внимание: если логин или пароль содержат спецсимволы (@, :, /), их нужно пропустить через encodeURIComponent, иначе парсер URL внутри undici некорректно разберёт строку подключения и вы получите ошибку аутентификации 407 вместо понятного сообщения.

Для задач, где важна стабильность сессии — например, авторизация на маркетплейсе с привязкой к одному IP на время сессии — удобнее брать резидентные прокси с sticky-сессией: IP не меняется заданное время, а значит сервис не видит «прыжков» между гео и не требует повторной капчи на каждом запросе.

Ротация прокси между запросами

Если у вас есть список из N прокси и задача — распределять запросы между ними, простейший вариант — round-robin без сторонних библиотек для очередей:

import { ProxyAgent, fetch } from 'undici';

const proxyList = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

let currentIndex = 0;

function getNextAgent() {
  const proxyUrl = proxyList[currentIndex];
  currentIndex = (currentIndex + 1) % proxyList.length;
  return new ProxyAgent(proxyUrl);
}

async function fetchWithRotation(url) {
  const agent = getNextAgent();
  return fetch(url, { dispatcher: agent });
}

for (let i = 0; i < 10; i++) {
  const res = await fetchWithRotation('https://httpbin.org/ip');
  console.log(await res.json());
}

Такой подход хорошо работает для парсинга карточек товаров или мониторинга цен, когда каждый следующий запрос логически независим от предыдущего. Для задач, где нужна высокая скорость смены IP на каждый запрос без ручного управления списком, проще использовать прокси с автоматической ротацией на стороне провайдера — тогда весь код выше сводится к одному статичному URL шлюза, а ротацию берёт на себя прокси дата-центра или резидентный пул с ротацией по таймеру.

Если нужна не просто круговая ротация, а взвешенная — например, отдавать приоритет более быстрым прокси — добавьте вес в структуру данных:

const weightedProxies = [
  { url: 'http://user:[email protected]:8000', weight: 5 },
  { url: 'http://user:[email protected]:8000', weight: 1 },
];

function pickWeighted(list) {
  const total = list.reduce((sum, p) => sum + p.weight, 0);
  let rnd = Math.random() * total;
  for (const p of list) {
    if (rnd < p.weight) return p.url;
    rnd -= p.weight;
  }
  return list[0].url;
}

Проверка живости прокси и исключение «мёртвых» IP

Прокси-пул со временем «протухает»: часть IP банится целевым сайтом, часть перестаёт отвечать. Чтобы ротация не гоняла запросы через мёртвые адреса, добавьте health-check с таймаутом через встроенный AbortController:

import { ProxyAgent, fetch } from 'undici';

async function isProxyAlive(proxyUrl, timeoutMs = 5000) {
  const agent = new ProxyAgent(proxyUrl);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch('https://httpbin.org/ip', {
      dispatcher: agent,
      signal: controller.signal,
    });
    return res.status === 200;
  } catch {
    return false;
  } finally {
    clearTimeout(timer);
  }
}

async function filterAliveProxies(proxyList) {
  const checks = await Promise.all(
    proxyList.map(async (url) => ({ url, alive: await isProxyAlive(url) }))
  );
  return checks.filter((c) => c.alive).map((c) => c.url);
}

Запускайте такую проверку раз в несколько минут фоновым таймером и обновляйте активный пул, а не весь статический список. Это особенно важно, если вы держите собственные прокси на дешёвых дата-центровых IP — там процент отвала выше, чем у управляемых провайдерских пулов.

SOCKS5 без сторонних библиотек

Важная оговорка: встроенный ProxyAgent в undici работает по HTTP CONNECT-туннелированию и «из коробки» понимает только HTTP/HTTPS-прокси. Протокол SOCKS5 устроен иначе — это бинарное рукопожатие на уровне TCP, и undici не разбирает его нативно. Если вам жёстко нужен SOCKS5 без установки socks-proxy-agent, можно реализовать минимальный коннектор вручную через модуль net и опцию connect у кастомного Agent:

import net from 'node:net';
import { Agent, fetch } from 'undici';

function socksHandshake(socket, targetHost, targetPort) {
  return new Promise((resolve, reject) => {
    // Шаг 1: приветствие, метод без аутентификации
    socket.write(Buffer.from([0x05, 0x01, 0x00]));

    socket.once('data', (greeting) => {
      if (greeting[1] !== 0x00) {
        return reject(new Error('SOCKS5: метод аутентификации не поддержан'));
      }

      const hostBuf = Buffer.from(targetHost, 'utf8');
      const request = Buffer.concat([
        Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
        hostBuf,
        Buffer.from([targetPort >> 8, targetPort & 0xff]),
      ]);

      socket.write(request);

      socket.once('data', (reply) => {
        if (reply[1] !== 0x00) {
          return reject(new Error('SOCKS5: подключение отклонено'));
        }
        resolve(socket);
      });
    });
  });
}

const socksAgent = new Agent({
  connect: (options, callback) => {
    const socket = net.connect(1080, 'socks-proxy.example.com', async () => {
      try {
        await socksHandshake(socket, options.hostname, 443);
        callback(null, socket);
      } catch (err) {
        callback(err, null);
      }
    });
    socket.on('error', (err) => callback(err, null));
  },
});

const response = await fetch('https://httpbin.org/ip', { dispatcher: socksAgent });
console.log(await response.json());

Это упрощённый handshake без аутентификации по логину и паролю (метод 0x02) и без поддержки IPv6 — для продакшена стоит дописать ветку с аутентификацией и обработкой разных типов адресов. Но для понимания механики — этого достаточно: SOCKS5 не требует TLS внутри туннеля на этапе рукопожатия, в отличие от HTTP CONNECT, поэтому код короче, чем кажется на первый взгляд.

На практике SOCKS5 чаще используют не для парсинга веб-страниц, а для проброса трафика мобильных приложений и антидетект-браузеров вроде Dolphin Anty или AdsPower, где протокол нужен для работы на уровне сокетов, а не HTTP-запросов. Если ваша задача именно HTTP/HTTPS-трафик в Node.js, встроенного ProxyAgent обычно достаточно, и возиться с ручным SOCKS5-коннектором нет смысла.

Обработка таймаутов и retry-логика

Прокси обрывают соединения чаще, чем прямое подключение — это нормально для residential-сетей, где узел может уйти в офлайн в любой момент. Нужна retry-обёртка с экспоненциальной задержкой:

import { ProxyAgent, fetch } from 'undici';

async function fetchWithRetry(url, proxyList, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const proxyUrl = proxyList[attempt % proxyList.length];
    const agent = new ProxyAgent(proxyUrl);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 8000);

    try {
      const res = await fetch(url, { dispatcher: agent, signal: controller.signal });
      clearTimeout(timer);
      if (res.status >= 500) throw new Error(`Server error: ${res.status}`);
      return res;
    } catch (err) {
      clearTimeout(timer);
      lastError = err;
      const delay = 500 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error(`Все попытки исчерпаны: ${lastError?.message}`);
}

Такая обёртка заодно решает проблему ротации при ошибках: если текущий прокси отвалился, следующая попытка автоматически уходит на другой IP из списка, а не долбится в тот же мёртвый узел.

Сравнение undici ProxyAgent, https-proxy-agent и socks-proxy-agent

Инструмент Зависимости Поддержка SOCKS5 HTTP/2 Скорость
undici ProxyAgent нет (встроен в Node 18+) нет из коробки да высокая
https-proxy-agent 1-2 пакета нет нет средняя
socks-proxy-agent 3-4 пакета да нет средняя

Для чистого HTTP/HTTPS-парсинга с минимальным числом зависимостей undici ProxyAgent выигрывает за счёт встроенности и совместимости с HTTP/2. Для SOCKS5 в продакшене проще взять проверенный socks-proxy-agent, чем поддерживать самописный handshake — ручная реализация из раздела выше хороша для обучения и редких edge-кейсов, но не для нагруженного продакшена с тысячами соединений в час.

Практические кейсы: парсинг и гео-тестирование

Мониторинг цен на маркетплейсах. Скрипт на undici с ротацией прокси и retry-логикой из разделов выше идеально подходит для обхода rate-limit на Wildberries и Ozon: каждый следующий запрос карточки товара уходит с нового IP, а health-check исключает забаненные адреса из пула автоматически.

Тестирование рекламных креативов по гео. Если нужно проверить, как отдаётся контент пользователю из конкретной страны для Google Ads или Facebook Ads, достаточно смёнить URL прокси в ProxyAgent на узел нужного региона — никакого браузера поднимать не нужно, если проверка идёт через API или HTML-ответ сервера.

Проверка доступности контента в разных странах. Для задач, где сайт отдаёт разный контент в зависимости от IP, резидентные подключения выглядят для целевого сервера неотличимыми от обычного пользователя, что снижает риск блокировки по сравнению с дата-центровыми диапазонами. Такой сценарий особенно актуален для мобильных прокси, если целевой сервис проверяет тип сети и блокирует явные дата-центровые подсети.

Массовая проверка API-эндпоинтов. При нагрузочном тестировании собственного API через внешний прокси-пул важно следить за connection pooling — undici по умолчанию переиспользует TCP-соединения внутри одного ProxyAgent, что ощутимо экономит время на TLS-рукопожатиях при сотнях последовательных запросов к одному хосту.

Заключение

Встроенный undici ProxyAgent закрывает большинство задач с HTTP/HTTPS-прокси в Node.js без необходимости тащить сторонние пакеты: аутентификация через URL, туннелирование через CONNECT, совместимость с HTTP/2 и connection pooling работают сразу. Ротацию и health-check прокси легко собрать на чистых промисах и таймерах, а SOCKS5 при необходимости реализуется вручную через кастомный connect-коннектор, хотя для продакшена разумнее взять готовую библиотеку именно под SOCKS.

Если ваш скрипт на undici предназначен для парсинга маркетплейсов или мониторинга цен, обратите внимание на резидентные прокси — они устойчивее к антибот-системам, чем классические дата-центровые IP, и реже требуют дополнительного решения капчи при массовых запросах к Wildberries, Ozon или Авито.