A maioria dos guias sobre proxies em Node.js recomenda adicionar ao projeto https-proxy-agent, socks-proxy-agent e mais alguns pacotes. Na prática, a partir da versão Node.js 18+, o cliente HTTP embutido undici já contém o ProxyAgent, que cobre a maioria das tarefas de parsing e teste de API através de proxies. Neste artigo, vamos discutir como configurar o tunelamento HTTP/HTTPS através do undici, organizar a rotação de IP entre requisições e o que fazer se você precisar de SOCKS5, sem querer adicionar dependências extras.
Por que undici em vez de axios e request
undici é um cliente HTTP/1.1 e HTTP/2, escrito pela equipe do Node.js e, a partir da versão 18, é a base do fetch global. Ele não tem dependências transitivas, é mais rápido que axios na maioria dos benchmarks de pooling de conexão e, o que é importante para o nosso tema, possui um módulo nativo para trabalhar com proxies através do tunelamento HTTP CONNECT.
Quando você faz parsing de Wildberries, Ozon ou testa criativos publicitários de diferentes regiões, cada pacote extra em node_modules é um ponto potencial de falha ao atualizar o Node.js. O undici vem junto com o runtime, portanto você obtém uma implementação atualizada de TLS, HTTP/2 e keep-alive sem o risco de que uma biblioteca abandonada quebre em uma nova versão major.
A instalação é extremamente simples:
npm install undici
Se você estiver usando Node.js 20+, parte da API está disponível globalmente sem importação, mas para o ProxyAgent a importação ainda é necessária, pois ele não é exportado no fetch global por padrão.
Configuração básica do ProxyAgent
Um exemplo mínimo de requisição através de um proxy é o seguinte:
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);
O ponto chave é o parâmetro dispatcher. Esta não é uma opção agent, como no antigo módulo http, mas sim o despachante do undici, que intercepta a conexão antes do handshake TLS e estabelece um túnel através do método CONNECT para o host de destino. Para requisições HTTPS através de um proxy, isso é crítico: um proxy HTTP comum não vê o conteúdo do tráfego criptografado, ele simplesmente repassa os bytes dentro do túnel TCP.
Se você estiver trabalhando com undici.request, que é de nível mais baixo, a sintaxe é semelhante:
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());
}
Para parsers em produção que fazem centenas de requisições por minuto, é importante não criar um novo ProxyAgent para cada requisição — isso prejudica o pooling de conexões. Crie uma instância do agente uma vez para o pool de conexões e reutilize-a entre as chamadas.
Autenticação de proxy com login e senha
A maioria dos provedores de proxy pagos fornece acesso por meio de um par de login/senha, em vez de IP-whitelisting. No undici, as credenciais são passadas diretamente na URL do 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());
Observe: se o login ou a senha contiverem caracteres especiais (@, :, /), eles devem ser passados através de encodeURIComponent, caso contrário, o parser de URL dentro do undici não analisará corretamente a string de conexão e você receberá um erro de autenticação 407 em vez de uma mensagem compreensível.
Para tarefas em que a estabilidade da sessão é importante — por exemplo, autorização em um marketplace vinculada a um único IP durante a sessão — é mais conveniente usar proxies residenciais com sessão sticky: o IP não muda por um período determinado, o que significa que o serviço não vê "saltos" entre regiões e não requer reCAPTCHA em cada requisição.
Rotação de proxies entre requisições
Se você tem uma lista de N proxies e a tarefa é distribuir as requisições entre eles, a opção mais simples é round-robin sem bibliotecas de terceiros para filas:
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());
}
Essa abordagem funciona bem para parsing de páginas de produtos ou monitoramento de preços, onde cada requisição seguinte é logicamente independente da anterior. Para tarefas que exigem alta velocidade de troca de IP em cada requisição sem gerenciamento manual da lista, é mais fácil usar proxies com rotação automática do lado do provedor — assim, todo o código acima se resume a uma única URL estática do gateway, e a rotação é feita por um proxy de datacenter ou pool residencial com rotação por timer.
Se você precisar de uma rotação não apenas circular, mas ponderada — por exemplo, dar prioridade a proxies mais rápidos — adicione peso à estrutura de dados:
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;
}
Verificação de saúde do proxy e exclusão de IPs "mortos"
O pool de proxies, com o tempo, "expira": alguns IPs são banidos pelo site de destino, outros param de responder. Para que a rotação não envie requisições através de endereços mortos, adicione uma verificação de saúde com timeout usando o AbortController embutido:
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);
}
Execute essa verificação a cada poucos minutos com um timer em segundo plano e atualize o pool ativo, em vez de toda a lista estática. Isso é especialmente importante se você mantiver seus próprios proxies em IPs de datacenter baratos — lá, a taxa de falhas é maior do que em pools gerenciados por provedores.
SOCKS5 sem bibliotecas de terceiros
Uma importante ressalva: o ProxyAgent embutido no undici funciona através do tunelamento HTTP CONNECT e "de fábrica" entende apenas proxies HTTP/HTTPS. O protocolo SOCKS5 funciona de maneira diferente — é um handshake binário no nível TCP, e o undici não o analisa nativamente. Se você realmente precisa de SOCKS5 sem instalar socks-proxy-agent, você pode implementar um conector mínimo manualmente através do módulo net e da opção connect em um Agent personalizado:
import net from 'node:net';
import { Agent, fetch } from 'undici';
function socksHandshake(socket, targetHost, targetPort) {
return new Promise((resolve, reject) => {
// Passo 1: saudação, método sem autenticação
socket.write(Buffer.from([0x05, 0x01, 0x00]));
socket.once('data', (greeting) => {
if (greeting[1] !== 0x00) {
return reject(new Error('SOCKS5: método de autenticação não suportado'));
}
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: conexão recusada'));
}
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());
Este é um handshake simplificado sem autenticação por login e senha (método 0x02) e sem suporte a IPv6 — para produção, é aconselhável adicionar uma ramificação com autenticação e tratamento de diferentes tipos de endereços. Mas para entender a mecânica — isso é suficiente: SOCKS5 não requer TLS dentro do túnel na fase de handshake, ao contrário do HTTP CONNECT, portanto o código é mais curto do que parece à primeira vista.
Na prática, o SOCKS5 é mais utilizado não para parsing de páginas da web, mas para redirecionar o tráfego de aplicativos móveis e navegadores anti-detect, como Dolphin Anty ou AdsPower, onde o protocolo é necessário para operar no nível dos sockets, e não de requisições HTTP. Se sua tarefa é especificamente tráfego HTTP/HTTPS em Node.js, o ProxyAgent embutido geralmente é suficiente, e não há sentido em lidar com um conector SOCKS5 manualmente.
Tratamento de timeouts e lógica de retry
Proxies quebram conexões mais frequentemente do que uma conexão direta — isso é normal para redes residenciais, onde um nó pode ficar offline a qualquer momento. É necessário um wrapper de retry com atraso exponencial:
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(`Erro de servidor: ${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(`Todas as tentativas esgotadas: ${lastError?.message}`);
}
Esse wrapper também resolve o problema de rotação em caso de erros: se o proxy atual falhar, a próxima tentativa automaticamente vai para outro IP da lista, em vez de continuar tentando o mesmo nó morto.
Comparação entre undici ProxyAgent, https-proxy-agent e socks-proxy-agent
| Ferramenta | Dependências | Suporte a SOCKS5 | HTTP/2 | Velocidade |
|---|---|---|---|---|
| undici ProxyAgent | nenhuma (embutido no Node 18+) | não por padrão | sim | alta |
| https-proxy-agent | 1-2 pacotes | não | não | média |
| socks-proxy-agent | 3-4 pacotes | sim | não | média |
Para parsing puro de HTTP/HTTPS com o mínimo de dependências, o undici ProxyAgent se destaca pela sua embutidura e compatibilidade com HTTP/2. Para SOCKS5 em produção, é mais fácil usar o comprovado socks-proxy-agent do que manter um handshake feito à mão — a implementação manual da seção acima é boa para aprendizado e casos raros, mas não para produção pesada com milhares de conexões por hora.
Casos práticos: parsing e geo-teste
Monitoramento de preços em marketplaces. Um script em undici com rotação de proxies e lógica de retry das seções acima é ideal para contornar o rate-limit em Wildberries e Ozon: cada requisição seguinte do cartão de produto é feita com um novo IP, e a verificação de saúde exclui automaticamente endereços banidos do pool.
Teste de criativos publicitários por geo. Se você precisa verificar como o conteúdo é entregue a um usuário de um país específico para Google Ads ou Facebook Ads, basta mudar a URL do proxy no ProxyAgent para um nó da região desejada — não é necessário levantar um navegador se a verificação for feita através da API ou da resposta HTML do servidor.
Verificação de disponibilidade de conteúdo em diferentes países. Para tarefas em que o site entrega conteúdo diferente dependendo do IP, as conexões residenciais parecem para o servidor de destino indistinguíveis de um usuário comum, o que reduz o risco de bloqueio em comparação com intervalos de datacenter. Esse cenário é especialmente relevante para proxies móveis, se o serviço de destino verifica o tipo de rede e bloqueia sub-redes de datacenter óbvias.
Verificação em massa de endpoints de API. Ao realizar testes de carga na sua própria API através de um pool de proxies externo, é importante monitorar o pooling de conexões — o undici, por padrão, reutiliza conexões TCP dentro de um único ProxyAgent, o que economiza tempo em handshakes TLS durante centenas de requisições sequenciais para um único host.
Conclusão
O undici ProxyAgent embutido cobre a maioria das tarefas com proxies HTTP/HTTPS em Node.js sem a necessidade de adicionar pacotes de terceiros: autenticação via URL, tunelamento através de CONNECT, compatibilidade com HTTP/2 e pooling de conexões funcionam imediatamente. A rotação e a verificação de saúde do proxy podem ser facilmente montadas com promessas e timers limpos, e o SOCKS5, se necessário, pode ser implementado manualmente através de um conector connect personalizado, embora para produção seja mais sensato usar uma biblioteca pronta especificamente para SOCKS.
Se seu script em undici é destinado ao parsing de marketplaces ou monitoramento de preços, considere usar proxies residenciais — eles são mais resistentes a sistemas anti-bot do que IPs de datacenter clássicos e raramente exigem soluções adicionais de CAPTCHA em requisições em massa para Wildberries, Ozon ou Avito.