← Retour au blog

Undici ProxyAgent en Node.js : configuration de proxy HTTP/SOCKS5 et rotation d'IP sans dépendances supplémentaires

Nous expliquons comment utiliser l'agent ProxyAgent intégré d'undici dans Node.js pour travailler avec des proxies HTTP et SOCKS5, organiser la rotation des IP et se passer de https-proxy-agent et socks-proxy-agent.

📅18 octobre 2026

La plupart des guides sur les proxies dans Node.js recommandent d'ajouter au projet https-proxy-agent, socks-proxy-agent et quelques autres paquets en plus. En réalité, à partir de la version Node.js 18+, le client HTTP intégré undici contient déjà ProxyAgent, qui couvre la plupart des tâches de parsing et de test d'API via des proxies. Dans cet article, nous allons voir comment configurer le tunneling HTTP/HTTPS via undici, organiser la rotation IP entre les requêtes et que faire si vous avez besoin de SOCKS5 sans vouloir ajouter de dépendance supplémentaire.

Pourquoi undici au lieu d'axios et request

undici est un client HTTP/1.1 et HTTP/2, développé par l'équipe de Node.js et, depuis la version 18, il est à la base du fetch global. Il n'a pas de dépendances transitives, il est plus rapide qu'axios dans la plupart des benchmarks sur le pooling de connexions, et ce qui est important pour notre sujet — il dispose d'un module natif pour travailler avec des proxies via le tunneling HTTP CONNECT.

Lorsque vous parsez Wildberries, Ozon ou testez des créations publicitaires de différentes géographies, chaque paquet supplémentaire dans node_modules est un point de défaillance potentiel lors de la mise à jour de Node.js. undici est livré avec le runtime, donc vous obtenez une implémentation à jour de TLS, HTTP/2 et keep-alive sans risque qu'une bibliothèque abandonnée ne casse sur une nouvelle version majeure.

L'installation est très simple :

npm install undici

Si vous avez Node.js 20+, une partie de l'API est disponible globalement sans importation, mais pour ProxyAgent, l'importation est tout de même nécessaire, car il n'est pas exporté dans le fetch global par défaut.

Configuration de base de ProxyAgent

Un exemple de requête fonctionnelle minimale via un proxy ressemble à ceci :

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);

Le point clé est le paramètre dispatcher. Ce n'est pas une option agent, comme dans l'ancien module http, mais bien le dispatcher d'undici, qui intercepte la connexion avant la poignée de main TLS et établit un tunnel via la méthode CONNECT vers l'hôte cible. Pour les requêtes HTTPS via un proxy, c'est critique : un proxy HTTP ordinaire ne voit pas le contenu du trafic chiffré, il se contente de faire passer les octets à l'intérieur du tunnel TCP.

Si vous travaillez non pas avec fetch, mais avec le bas niveau undici.request, la syntaxe est similaire :

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());
}

Pour les parseurs en production qui effectuent des centaines de requêtes par minute, il est important de ne pas créer un nouveau ProxyAgent pour chaque requête — cela tue le pooling de connexions. Créez une instance de l'agent une fois pour le pool de connexions et réutilisez-la entre les appels.

Authentification du proxy par login et mot de passe

La plupart des fournisseurs de proxies payants fournissent l'accès par paire login/mot de passe, plutôt que par IP-whitelisting. Dans undici, les identifiants sont transmis directement dans l'URL du proxy :

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());

Notez que si le login ou le mot de passe contient des caractères spéciaux (@, :, /), ils doivent être échappés via encodeURIComponent, sinon le parseur d'URL à l'intérieur d'undici analysera incorrectement la chaîne de connexion et vous obtiendrez une erreur d'authentification 407 au lieu d'un message compréhensible.

Pour les tâches où la stabilité de la session est importante — par exemple, l'autorisation sur un marketplace liée à une seule IP pendant la durée de la session — il est plus pratique d'utiliser des proxies résidentiels avec une session sticky : l'IP ne change pas pendant un temps donné, ce qui signifie que le service ne voit pas de « sauts » entre les géographies et ne nécessite pas de captcha répétée à chaque requête.

Rotation des proxies entre les requêtes

Si vous avez une liste de N proxies et que votre tâche est de répartir les requêtes entre eux, la solution la plus simple est le round-robin sans bibliothèques tierces pour les files d'attente :

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());
}

Cette approche fonctionne bien pour le parsing des fiches produits ou la surveillance des prix, lorsque chaque requête suivante est logiquement indépendante de la précédente. Pour les tâches où une vitesse élevée de changement d'IP est nécessaire pour chaque requête sans gestion manuelle de la liste, il est plus simple d'utiliser des proxies avec rotation automatique du côté du fournisseur — alors tout le code ci-dessus se réduit à une seule URL statique du gateway, et la rotation est gérée par un proxy de datacenter ou un pool résidentiel avec rotation par minuteur.

Si vous avez besoin non seulement d'une rotation circulaire, mais d'une rotation pondérée — par exemple, donner la priorité aux proxies plus rapides — ajoutez un poids dans la structure de données :

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;
}

Vérification de la vivacité des proxies et exclusion des IP « mortes »

Un pool de proxies se « périme » avec le temps : certaines IP sont bannies par le site cible, d'autres cessent de répondre. Pour que la rotation ne fasse pas passer des requêtes par des adresses mortes, ajoutez une vérification de santé avec un timeout via le AbortController intégré :

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);
}

Exécutez cette vérification toutes les quelques minutes avec un minuteur en arrière-plan et mettez à jour le pool actif, et non toute la liste statique. Cela est particulièrement important si vous maintenez vos propres proxies sur des IP de datacenter bon marché — le pourcentage de défaillance y est plus élevé que pour les pools gérés par des fournisseurs.

SOCKS5 sans bibliothèques tierces

Une importante mise en garde : le ProxyAgent intégré dans undici fonctionne via le tunneling HTTP CONNECT et « par défaut » ne comprend que les proxies HTTP/HTTPS. Le protocole SOCKS5 fonctionne différemment — c'est une poignée de main binaire au niveau TCP, et undici ne l'analyse pas nativement. Si vous avez absolument besoin de SOCKS5 sans installer socks-proxy-agent, vous pouvez implémenter un connecteur minimal manuellement via le module net et l'option connect dans un Agent personnalisé :

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

function socksHandshake(socket, targetHost, targetPort) {
  return new Promise((resolve, reject) => {
    // Étape 1 : salutation, méthode sans authentification
    socket.write(Buffer.from([0x05, 0x01, 0x00]));

    socket.once('data', (greeting) => {
      if (greeting[1] !== 0x00) {
        return reject(new Error('SOCKS5 : méthode d\'authentification non supportée'));
      }

      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 : connexion refusée'));
        }
        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());

C'est une poignée de main simplifiée sans authentification par login et mot de passe (méthode 0x02) et sans support IPv6 — pour la production, il vaut mieux ajouter une branche avec authentification et gestion de différents types d'adresses. Mais pour comprendre la mécanique — cela suffit : SOCKS5 ne nécessite pas de TLS à l'intérieur du tunnel au moment de la poignée de main, contrairement à HTTP CONNECT, donc le code est plus court qu'il n'y paraît au premier abord.

En pratique, SOCKS5 est souvent utilisé non pas pour parser des pages web, mais pour faire passer le trafic des applications mobiles et des navigateurs anti-détection comme Dolphin Anty ou AdsPower, où le protocole est nécessaire pour fonctionner au niveau des sockets, et non des requêtes HTTP. Si votre tâche concerne spécifiquement le trafic HTTP/HTTPS dans Node.js, le ProxyAgent intégré est généralement suffisant, et il n'est pas nécessaire de s'embêter avec un connecteur SOCKS5 manuel.

Gestion des timeouts et logique de retry

Les proxies interrompent les connexions plus souvent qu'une connexion directe — c'est normal pour les réseaux résidentiels, où un nœud peut passer hors ligne à tout moment. Une enveloppe de retry avec un délai exponentiel est nécessaire :

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(`Erreur serveur : ${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(`Toutes les tentatives ont échoué : ${lastError?.message}`);
}

Cette enveloppe résout également le problème de rotation en cas d'erreurs : si le proxy actuel échoue, la prochaine tentative passe automatiquement à une autre IP de la liste, au lieu de continuer à frapper le même nœud mort.

Comparaison entre undici ProxyAgent, https-proxy-agent et socks-proxy-agent

Outil Dépendances Support SOCKS5 HTTP/2 Vitesse
undici ProxyAgent aucun (intégré dans Node 18+) non par défaut oui élevée
https-proxy-agent 1-2 paquets non non moyenne
socks-proxy-agent 3-4 paquets oui non moyenne

Pour le parsing pur HTTP/HTTPS avec un nombre minimal de dépendances, undici ProxyAgent l'emporte grâce à son intégration et sa compatibilité avec HTTP/2. Pour SOCKS5 en production, il est plus simple de prendre un socks-proxy-agent éprouvé que de maintenir une poignée de main personnalisée — l'implémentation manuelle de la section ci-dessus est bonne pour l'apprentissage et des cas particuliers rares, mais pas pour une production chargée avec des milliers de connexions par heure.

Cas pratiques : parsing et tests géographiques

Surveillance des prix sur les marketplaces. Un script sur undici avec rotation des proxies et logique de retry des sections ci-dessus est idéal pour contourner les limites de taux sur Wildberries et Ozon : chaque requête suivante pour une fiche produit part avec une nouvelle IP, et la vérification de santé exclut automatiquement les adresses bannies du pool.

Test des créations publicitaires par géographie. Si vous devez vérifier comment le contenu est rendu à un utilisateur d'un pays spécifique pour Google Ads ou Facebook Ads, il suffit de changer l'URL du proxy dans le ProxyAgent vers un nœud de la région souhaitée — il n'est pas nécessaire de lancer un navigateur si la vérification se fait via l'API ou la réponse HTML du serveur.

Vérification de l'accessibilité du contenu dans différents pays. Pour les tâches où le site fournit un contenu différent selon l'IP, les connexions résidentielles apparaissent pour le serveur cible comme des utilisateurs ordinaires, ce qui réduit le risque de blocage par rapport aux plages d'IP de datacenter. Ce scénario est particulièrement pertinent pour les proxies mobiles, si le service cible vérifie le type de réseau et bloque les sous-réseaux de datacenter évidents.

Vérification massive des points de terminaison API. Lors des tests de charge de votre propre API via un pool de proxies externe, il est important de surveiller le pooling de connexions — undici réutilise par défaut les connexions TCP à l'intérieur d'un même ProxyAgent, ce qui fait économiser du temps sur les poignées de main TLS lors de centaines de requêtes consécutives vers un même hôte.

Conclusion

Le ProxyAgent intégré d'undici couvre la plupart des tâches avec des proxies HTTP/HTTPS dans Node.js sans avoir besoin d'ajouter des paquets tiers : authentification via URL, tunneling via CONNECT, compatibilité avec HTTP/2 et pooling de connexions fonctionnent immédiatement. La rotation et la vérification de santé des proxies peuvent être facilement mises en place avec des promesses pures et des minuteurs, et SOCKS5 peut être implémenté manuellement via un connecteur de connexion personnalisé si nécessaire, bien qu'il soit plus raisonnable de prendre une bibliothèque prête à l'emploi spécifiquement pour SOCKS en production.

Si votre script sur undici est destiné à parser des marketplaces ou à surveiller des prix, pensez aux proxies résidentiels — ils sont plus résistants aux systèmes anti-bots que les IP de datacenter classiques et nécessitent moins souvent une solution captcha supplémentaire lors de requêtes massives vers Wildberries, Ozon ou Avito.