Proxy no Puppeteer: autenticação, rotação e SOCKS5

Publicado:

15 min de leitura

Acar Diveroli
Autor: Acar Diveroli
O logotipo da Proxynet e o logo branco do Puppeteer lado a lado em uma moldura técnica, unidos por uma cruz fina

Você adiciona --proxy-server=http://user:pass@host:port aos argumentos de inicialização, do jeito que escreveria um proxy para o cURL, e o primeiro page.goto() para com net::ERR_NO_SUPPORTED_PROXIES. Você tira as credenciais, e o erro passa a ser net::ERR_INVALID_AUTH_CREDENTIALS. O Puppeteer não tem uma opção de proxy própria. Ele inicia o Chrome, e é o Chrome que decide como um proxy é usado; por isso a maioria das respostas está nas regras do Chrome, e não na API do Puppeteer.

Este artigo cobre a flag de inicialização e o page.authenticate(), um proxy por contexto de navegador, sessões fixas e rotativas, SOCKS5, a lista de bypass, a verificação do IP de saída, os erros que você vai encontrar e um script completo com novas tentativas. Todos os exemplos rodaram em 6 de outubro de 2026 com o Puppeteer 25.12.0 e a versão 154 do Chrome que ele baixa, contra um proxy HTTP local que exige usuário e senha e contra um servidor SOCKS5 local. As mensagens de erro citadas são as que obtivemos.

Como o Puppeteer envia o tráfego por um proxy?

O Puppeteer é uma biblioteca Node.js que controla um navegador real a partir do código. Ele não abre conexões de rede por conta própria; quem faz isso é o navegador. Por isso o proxy é uma configuração do Chrome, passada como flag de linha de comando quando o navegador inicia ou como opção quando um contexto de navegador é criado. Um contexto de navegador é uma sessão isolada dentro de um mesmo navegador, com cookies, cache e armazenamento próprios, parecida com uma janela anônima.

O login no proxy é uma etapa separada, porque o Chrome não lê credenciais do endereço do proxy. Nosso proxy de teste registrou esta sequência para uma página HTTPS:

  1. O Chrome envia CONNECT httpbin.org:443, pedindo ao proxy que abra um túnel até o site. Para uma página http://, ele envia a própria requisição.
  2. A requisição não leva credenciais, então o proxy responde 407 Proxy Authentication Required. A RFC 9110 define o 407 como o equivalente do 401 do lado do proxy.
  3. O Puppeteer captura esse desafio e responde com o usuário e a senha passados ao page.authenticate().
  4. O Chrome repete a requisição com um cabeçalho Proxy-Authorization, o proxy responde 200, e a conexão criptografada com o site é montada dentro do túnel. O proxy vê o nome do host, não o conteúdo da página.
  5. O Chrome guarda as credenciais no cache de autenticação do contexto e as envia de imediato nas conexões seguintes.

A etapa 5 explica por que as credenciais acabam pertencendo ao contexto, como mostram os testes mais abaixo.

Como configurar um proxy com --proxy-server e page.authenticate()?

npm i puppeteer instala a biblioteca e baixa um Chrome compatível; puppeteer-core é a mesma API sem esse download. A configuração mínima:

js
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://pr.proxynet.io:8000"],
});
const page = await browser.newPage();
await page.authenticate({ username: "user", password: "pass" });

await page.goto("https://httpbin.org/ip");
console.log(await page.$eval("body", (el) => el.innerText)); // o IP de saída que o site vê

await browser.close();

O http:// na flag descreve a conexão com o proxy, não as páginas. Um proxy HTTP também leva sites https:// pelo túnel, e o conteúdo deles continua criptografado entre o Chrome e o site. A referência de page.authenticate() acrescenta que, para isso, o Puppeteer liga nos bastidores a interceptação de requisições (request interception), o que pode custar um pouco de desempenho; passar null a desliga.

Três coisas deram errado nos nossos testes, e cada uma virou uma regra fixa:

  • Chame page.authenticate() antes do primeiro goto(). Quando chamado depois, a primeira navegação falhou com net::ERR_INVALID_AUTH_CREDENTIALS; só a seguinte passou.
  • Deixe as credenciais fora da flag. Com user:pass@ no endereço, o Chrome 154 rejeitou o valor inteiro com net::ERR_NO_SUPPORTED_PROXIES. Guias antigos descrevem um 407 nesse ponto; hoje a requisição nem chega ao proxy.
  • Não envie Proxy-Authorization por conta própria. Ao definir esse cabeçalho com page.setExtraHTTPHeaders(), o Chrome recusou a navegação com net::ERR_INVALID_ARGUMENT.
Valor de --proxy-serverResultado no Chrome 154
http://pr.proxynet.io:8000Funciona; páginas HTTPS passam por um túnel CONNECT
pr.proxynet.io:8000Funciona; sem esquema, o Chrome assume um proxy HTTP
http://user:pass@pr.proxynet.io:8000net::ERR_NO_SUPPORTED_PROXIES
socks5://host:portSó funciona quando o servidor não pede senha
socks5h://host:portnet::ERR_NO_SUPPORTED_PROXIES; o Chrome não conhece esse esquema

Como usar um proxy diferente em cada contexto de navegador?

O Puppeteer 22 renomeou createIncognitoBrowserContext(), o nome que guias antigos ainda usam, para browser.createBrowserContext(). As suas BrowserContextOptions trazem dois campos de proxy, proxyServer e proxyBypassList. O navegador pode iniciar sem proxy enquanto cada contexto recebe o seu:

js
import puppeteer from "puppeteer";

const PROXY = "http://pr.proxynet.io:8000";
// Uma sessão fixa por contexto: mesmo ID de sessão -> mesmo IP de saída por até 600 segundos
const sessions = ["a1b2c3", "d4e5f6"];

const browser = await puppeteer.launch(); // o navegador em si inicia sem proxy
for (const id of sessions) {
  const context = await browser.createBrowserContext({ proxyServer: PROXY });
  const page = await context.newPage();
  await page.authenticate({ username: `user-session-${id}-ttl-600`, password: "pass" });

  for (let i = 0; i < 2; i++) {
    await page.goto("https://httpbin.org/ip");
    console.log(id, await page.$eval("body", (el) => el.innerText));
  }
  await context.close(); // cookies, cache e a configuração de proxy vão embora com o contexto
}
await browser.close();

Nosso proxy de teste distribui as saídas por ID de sessão, como faz um gateway. Os dois contextos saíram por dois endereços diferentes, e cada um manteve o seu endereço na segunda requisição. Mais dois resultados importam em trabalhos reais:

  • As credenciais pertencem ao contexto, não à página. Uma segunda página no mesmo contexto chamou page.authenticate() com outro usuário, e mesmo assim todas as requisições dela continuaram levando o primeiro usuário. Uma página que nunca fez essa chamada passou com as mesmas credenciais em cache.
  • Um contexto sem proxyServer herda o proxy da flag de inicialização, mas mantém as próprias credenciais. Com --proxy-server no navegador, dois contextos com IDs de sessão próprios ainda saíram por duas saídas diferentes.
Flag --proxy-servercreateBrowserContext({ proxyServer })
AlcanceO navegador inteiroSó aquele contexto
Saídas separadas em um navegadorSim, um usuário por contextoSim, até hosts de proxy diferentes
Trocar o proxyReiniciar o navegadorFechar o contexto e abrir outro
Indicado paraUm script, uma saídaMuitas sessões independentes em paralelo

A regra prática: uma sessão, um contexto, um usuário.

Rotativa ou fixa: qual tipo de sessão combina com um navegador?

Um navegador não envia uma requisição por página. Uma página de produto puxa o documento HTML, scripts, folhas de estilo, imagens e chamadas de API em segundo plano, muitas vezes de vários hosts e por várias conexões. Em um gateway rotativo, cada nova conexão pode receber um novo IP de saída; no nosso teste, até o favicon de uma página saiu por um endereço diferente do da página. Para páginas sem relação entre si, isso não faz mal. Já em um login, um carrinho ou um formulário de várias etapas, o site vê um visitante pulando entre endereços e pode encerrar a sessão.

O gateway lê o tipo de sessão a partir do nome de usuário, que o Gerador de endpoints do painel escreve para você. As partes são fáceis de ler: -country-de escolhe o país de saída (a cidade é escolhida no mesmo gerador), -session-a1b2c3-ttl-600 mantém uma saída para esse ID de sessão pelo número de segundos indicado, de 1 a 60 minutos, e um usuário sem a parte de sessão faz rotação.

Para páginas independentes, como listas de categorias ou resultados de busca abertos em contextos de vida curta, use os Proxies rotativos.

Para tudo o que guarda estado, use os Proxies de sessão fixa: um contexto, um ID de sessão, mantido por mais tempo do que o fluxo leva. Quando o trabalho terminar, feche o contexto para que os cookies e o IP terminem juntos.

A rotação distribui trabalho independente entre saídas; ela não é um jeito de contornar os limites de um site. Um 429 Too Many Requests ou um 403 Forbidden pede que você diminua o ritmo, e um IP novo não muda essa resposta.

O Puppeteer funciona com proxy SOCKS5?

Sim, mas sem usuário e senha. A documentação de proxy do Chromium afirma que nenhum método de autenticação é suportado para SOCKSv5. Nosso servidor SOCKS5 local viu isso do outro lado: a saudação do Chrome oferecia apenas o método 0x00, "sem autenticação". Um servidor que exigia o método de usuário e senha (0x02) tinha de recusar essa oferta, e a navegação falhava com net::ERR_SOCKS_CONNECTION_FAILED. O page.authenticate() não fez diferença, já que o SOCKS5 não tem um desafio no estilo do 407 para o Puppeteer responder.

A solução é provar quem você é pelo endereço IP. Adicione o IP público da máquina que roda o Chrome à whitelist de IP no painel (até 10 endereços), pegue a porta SOCKS5 que o Gerador de endpoints mostra e inicie o navegador sem credenciais:

js
import puppeteer from "puppeteer";

// Sem usuário nem senha: o IP público desta máquina precisa estar na whitelist de IP.
// SOCKS5_PORT: a porta SOCKS5 mostrada no Gerador de endpoints.
const { SOCKS5_PORT } = process.env;

const browser = await puppeteer.launch({
  args: [`--proxy-server=socks5://pr.proxynet.io:${SOCKS5_PORT}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip");
console.log(await page.$eval("body", (el) => el.innerText));
await browser.close();

O Chrome enviou nomes de host ao servidor SOCKS5, e não endereços IP; assim o DNS é resolvido no proxy, e o hábito de socks5h:// vindo do cURL não é necessário. Para páginas web, um proxy HTTP costuma ser a escolha mais simples, porque funciona com page.authenticate().

Como deixar alguns endereços fora do proxy?

A flag --proxy-bypass-list, ou proxyBypassList como array em um contexto, lista os hosts que o Chrome acessa diretamente. Na flag, as regras são separadas por ponto e vírgula ou por vírgula: --proxy-bypass-list=*.internal.example;192.168.0.0/16. No nosso teste, um host da lista ignorou o proxy por completo e foi resolvido na máquina local.

O Chrome também tem regras implícitas: localhost, *.localhost, 127.0.0.1/8 e [::1] nunca usam o proxy, mesmo com a lista vazia. Se você testa contra um servidor local e o log do proxy fica em silêncio, o motivo é esse. A regra especial <-loopback> remove essas regras implícitas; com ela, nossa requisição para 127.0.0.1 passou pelo proxy.

Como verificar o IP de saída?

Compare, no mesmo navegador, um contexto direto com um contexto que passa pelo proxy. Se os endereços forem diferentes, o tráfego está passando pelo proxy:

js
import puppeteer from "puppeteer";

async function exitIp(context, credentials) {
  const page = await context.newPage();
  if (credentials) await page.authenticate(credentials);
  await page.goto("https://httpbin.org/ip");
  const { origin } = JSON.parse(await page.$eval("body", (el) => el.innerText));
  await page.close();
  return origin;
}

const browser = await puppeteer.launch();
const direct = await browser.createBrowserContext();
const proxied = await browser.createBrowserContext({ proxyServer: "http://pr.proxynet.io:8000" });

const own = await exitIp(direct);
const viaProxy = await exitIp(proxied, { username: "user", password: "pass" });
console.log({ own, viaProxy, proxyWorks: own !== viaProxy });
await browser.close();

Se o seu alvo é um país, consulte também o IP de saída em um serviço de geolocalização. Quando a verificação falhar, tire o Chrome da equação: Seu proxy está funcionando? Como testar um proxy traz os testes de linha de comando que separam um problema de rede de um problema de código.

Quais erros você vai ver e o que eles significam?

Provocamos cada um deles no ambiente de teste local:

O que você vêO que aconteceuO que fazer
net::ERR_PROXY_CONNECTION_FAILEDO Chrome não alcançou o proxy: host ou porta errados, ou o tráfego de saída está bloqueadoConfira o endereço e a porta; tentar de novo não ajuda
net::ERR_INVALID_AUTH_CREDENTIALSO proxy pediu login e o Chrome não tinha credenciaisChame page.authenticate() antes do primeiro goto()
goto() resolve com status 407A senha estava errada; o Puppeteer tentou uma vez e desistiuConfira response.status() e corrija as credenciais
net::ERR_TUNNEL_CONNECTION_FAILEDO login funcionou, mas o proxy não conseguiu abrir o túnel até o siteConfira o endereço de destino
net::ERR_NO_SUPPORTED_PROXIESO Chrome rejeitou o valor de --proxy-serverRemova user:pass@, use http:// ou socks5://
net::ERR_SOCKS_CONNECTION_FAILEDO servidor SOCKS5 quer senha, ou o seu IP não está na whitelistColoque o seu IP na whitelist ou use a porta HTTP
A página carrega, mas o IP é o seuUma regra de bypass ou um host de loopback ignorou o proxyConfira a lista de bypass e o <-loopback>

A linha da senha errada é a que fica escondida por mais tempo: nada lança exceção, e o script segue em frente com a página 407 do proxy. Em um navegador de desktop, o erro de túnel tem mais causas, da verificação HTTPS do antivírus a um proxy do sistema esquecido; elas estão em Como resolver err_tunnel_connection_failed no Chrome e Edge. Durante o desenvolvimento, dois listeners mostram o erro real por trás de um tempo esgotado:

js
page.on("requestfailed", (req) => console.log("FALHOU", req.url(), req.failure()?.errorText));
page.on("response", (res) => res.status() >= 400 && console.log(res.status(), res.url()));

Exemplo completo: contextos em paralelo, sessões fixas e novas tentativas

O script abre seis páginas geradas com JavaScript, com no máximo três contextos ao mesmo tempo. Cada tentativa recebe um contexto novo e uma sessão fixa nova, de modo que os cookies e o IP de saída mudam juntos. Imagens, mídia e fontes são bloqueadas para economizar tráfego; page.setRequestInterception() faz isso sem quebrar o page.authenticate(). Tempos esgotados e falhas de túnel são repetidos com espera exponencial, enquanto erros de configuração e um 403 ou 429 do site encerram o trabalho.

js
import puppeteer from "puppeteer";

const PROXY = "http://pr.proxynet.io:8000";
const USER = "user";
const PASS = "pass";
const URLS = Array.from({ length: 6 }, (_, i) => `https://quotes.toscrape.com/js/page/${i + 1}/`);
const CONCURRENCY = 3; // contextos abertos ao mesmo tempo
const ATTEMPTS = 3;
const BLOCKED = new Set(["image", "media", "font"]);
// Erros de configuração: esperar e tentar de novo não resolve
const FATAL = ["ERR_PROXY_CONNECTION_FAILED", "ERR_NO_SUPPORTED_PROXIES", "ERR_INVALID_AUTH_CREDENTIALS"];

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function scrape(browser, url) {
  for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
    // Um contexto por tentativa: cookies próprios e sessão fixa própria (um IP de saída)
    const context = await browser.createBrowserContext({ proxyServer: PROXY });
    try {
      const page = await context.newPage();
      const session = crypto.randomUUID().slice(0, 8);
      await page.authenticate({ username: `${USER}-session-${session}-ttl-300`, password: PASS });
      await page.setRequestInterception(true);
      page.on("request", (req) => (BLOCKED.has(req.resourceType()) ? req.abort() : req.continue()));

      const res = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
      const status = res ? res.status() : 0;
      if (status === 403 || status === 429) {
        // O site está recusando ou freando você: um IP novo não é a resposta
        throw Object.assign(new Error(`HTTP ${status}: pare e reduza o ritmo de requisições`), { fatal: true });
      }
      if (status >= 400) throw new Error(`HTTP ${status}`);

      await page.waitForSelector("div.quote span.text", { timeout: 10_000 }); // gerado com JavaScript
      return await page.$$eval("div.quote span.text", (els) => els.map((el) => el.textContent));
    } catch (err) {
      err.fatal ||= FATAL.some((code) => err.message.includes(code));
      if (err.fatal || attempt === ATTEMPTS) throw err;
      await sleep(2 ** attempt * 1000 + Math.random() * 1000); // espera antes da próxima tentativa
    } finally {
      await context.close();
    }
  }
}

const browser = await puppeteer.launch();
const queue = [...URLS];
const results = new Map();
await Promise.all(
  Array.from({ length: CONCURRENCY }, async () => {
    while (queue.length) {
      const url = queue.shift();
      try {
        results.set(url, `${(await scrape(browser, url)).length} citações`);
      } catch (err) {
        results.set(url, `ERRO ${err.message.split("\n")[0]}`);
        if (err.fatal) queue.length = 0; // para o trabalho inteiro, não só esta página
      }
    }
  }),
);
await browser.close();
for (const url of URLS) console.log(url, results.get(url) ?? "ignorada");

Pelo proxy de teste local, as seis páginas voltaram com dez citações cada uma em menos de cinco segundos. Com a porta do proxy digitada errada de propósito, as três páginas em andamento pararam na hora com net::ERR_PROXY_CONNECTION_FAILED, e as outras três foram ignoradas; uma página de teste local que respondia 429 encerrou o trabalho da mesma forma. Comece com um valor baixo de CONCURRENCY: cada contexto ocupa memória, e o site de destino também tem limites.

Casos de uso

  • Páginas de catálogo e de preços geradas com JavaScript, que um cliente HTTP simples vê vazias.
  • Verificar como o seu próprio site, os seus preços ou os seus banners de consentimento aparecem em outro país, com um contexto por país.
  • Capturas de tela e PDFs de páginas públicas do jeito que um visitante de determinado país as vê.
  • Monitorar as suas próprias páginas e fluxos de checkout de fora da sua rede.

A mesma moldura vale para todos: respeite o robots.txt e os termos do site, use uma API oficial quando ela existir e mantenha o ritmo de requisições em um nível que o site aguente.

Erros comuns

  • Escrever user:pass@ em --proxy-server. O Chrome rejeita o valor com net::ERR_NO_SUPPORTED_PROXIES.
  • Esperar um usuário novo por página. O Chrome mantém as primeiras credenciais para o contexto inteiro; use um contexto por sessão.
  • Tentar uma senha com SOCKS5. O Chrome oferece só "sem autenticação"; use a whitelist de IP ou a porta HTTP.
  • Testar contra localhost e confiar no resultado. Endereços de loopback ignoram o proxy, a menos que você adicione <-loopback>.
  • Tratar uma resposta 407 como se fosse uma página. Uma senha errada não lança exceção; confira response.status().
  • Trocar o IP depois de um 429 ou 403. Diminua o ritmo; o limite tem a ver com o seu comportamento, não com o seu endereço.
  • Deixar contextos abertos. Cada um ocupa memória; feche-os em um bloco finally.

Guia de decisão

NecessidadeRecomendação
Um script, uma saídaFlag --proxy-server mais page.authenticate()
Muitas sessões independentes em um navegadorcreateBrowserContext({ proxyServer }), um ID de sessão por contexto
Muitas páginas que não dependem umas das outrasUsuário rotativo, contextos de vida curta
Login, carrinho ou formulário de várias etapasSessão fixa mais longa que o fluxo, um contexto
SOCKS5 é obrigatórioWhitelist de IP, sem credenciais
Uma ferramenta que só aceita a flag de inicializaçãoWhitelist de IP ou um encaminhador local como proxy-chain
Hosts internos precisam ficar diretos--proxy-bypass-list ou proxyBypassList

Perguntas frequentes

O puppeteer.launch() tem uma opção de proxy?

Não. O proxy vai em args como a flag --proxy-server do Chrome, e o login vai em page.authenticate(). A API do próprio Puppeteer só tem campos de proxy em browser.createBrowserContext(): proxyServer e proxyBypassList.

Cada página pode ter o seu próprio proxy?

Não dentro de um mesmo contexto. O proxy e as credenciais em cache pertencem ao contexto, então no nosso teste uma segunda página com outras credenciais continuou usando as primeiras. Dê a cada página que precisa de uma saída própria o seu próprio contexto; um contexto custa muito menos que um navegador novo.

O que é o proxy-chain e você precisa dele?

proxy-chain é um pacote Node.js de código aberto cuja função anonymizeProxy() inicia um proxy local sem senha e encaminha o tráfego para um proxy upstream com credenciais, de modo que o Chrome recebe um endereço simples http://127.0.0.1:<port>. No nosso teste, funcionou tanto com um upstream HTTP quanto com um SOCKS5 que exigiam senha. Com page.authenticate() ou uma whitelist de IP, você não precisa dele.

Dá para trocar o proxy sem reiniciar o navegador?

Sim, por meio de contextos. Feche o contexto, abra um novo com outro proxyServer ou outro ID de sessão e chame page.authenticate() de novo. Só a flag --proxy-server fica fixa durante toda a vida do navegador.

Por que o log do proxy mostra requisições que você não fez?

O tráfego de fundo do próprio Chrome, como verificações de atualização e requisições a serviços do Google, vai para o mesmo proxy. No nosso log, a maior parte chegou sem credenciais e recebeu um 407, porque page.authenticate() responde apenas aos desafios do tráfego das páginas. Isso não afeta as suas páginas.

Um proxy acaba com os CAPTCHAs no Puppeteer?

Não. Um proxy muda de onde o tráfego vem, enquanto os CAPTCHAs também reagem ao ritmo de requisições, aos sinais do navegador e a sessões inconsistentes. As causas e as formas legítimas de reduzi-los estão em Puppeteer e CAPTCHA: por que aparece, como reduzir?.

Em resumo

No Puppeteer, o proxy é uma configuração do Chrome: a flag --proxy-server para o navegador inteiro ou proxyServer para um contexto, com o login em page.authenticate() antes da primeira navegação. As credenciais ficam em cache por contexto, então monte cada sessão como um contexto, escolha sessão fixa ou rotativa pelo nome de usuário e use uma whitelist de IP para SOCKS5. Confira response.status() e mantenha um ritmo de requisições educado. Para páginas que devem abrir a partir de conexões domésticas comuns, as saídas vêm de um pool de Proxies residenciais.