Proxy en Puppeteer: autenticación, rotación y SOCKS5

Publicado:

15 min de lectura

Acar Diveroli
Autor: Acar Diveroli
El logotipo de Proxynet y el logo blanco de Puppeteer, uno junto al otro en un marco técnico, unidos por una cruz fina

Añades --proxy-server=http://user:pass@host:port a los argumentos de inicio, como escribirías un proxy para cURL, y el primer page.goto() se detiene con net::ERR_NO_SUPPORTED_PROXIES. Quitas las credenciales y el error pasa a ser net::ERR_INVALID_AUTH_CREDENTIALS. Puppeteer no tiene una opción de proxy propia. Inicia Chrome, y es Chrome quien decide cómo se usa un proxy, así que la mayoría de las respuestas están en las reglas de Chrome y no en la API de Puppeteer.

Este artículo cubre el flag de inicio y page.authenticate(), un proxy por contexto de navegador, las sesiones fijas y rotativas, SOCKS5, la lista de bypass, la comprobación de la IP de salida, los errores que vas a encontrar y un script completo con reintentos. Todos los ejemplos se ejecutaron el 6 de octubre de 2026 con Puppeteer 25.12.0 y la versión de Chrome 154 que descarga, contra un proxy HTTP local que exige usuario y contraseña y contra un servidor SOCKS5 local. Los mensajes de error citados son los que obtuvimos.

¿Cómo envía Puppeteer el tráfico a través de un proxy?

Puppeteer es una biblioteca de Node.js que controla un navegador real desde el código. No abre conexiones de red por sí misma; eso lo hace el navegador. Por eso un proxy es un ajuste de Chrome, que se indica como flag de línea de comandos al iniciar el navegador o como opción al crear un contexto de navegador. Un contexto de navegador es una sesión aislada dentro de un mismo navegador, con sus propias cookies, caché y almacenamiento, parecida a una ventana de incógnito.

Iniciar sesión en el proxy es un paso aparte, porque Chrome no lee las credenciales de la dirección del proxy. Nuestro proxy de prueba registró esta secuencia para una página HTTPS:

  1. Chrome envía CONNECT httpbin.org:443 y le pide así al proxy que abra un túnel hacia el sitio. Para una página http:// envía directamente la petición.
  2. La petición no lleva credenciales, así que el proxy responde 407 Proxy Authentication Required. RFC 9110 define el 407 como el equivalente del 401 en el lado del proxy.
  3. Puppeteer captura ese desafío y responde con el usuario y la contraseña que recibió page.authenticate().
  4. Chrome repite la petición con una cabecera Proxy-Authorization, el proxy responde 200 y la conexión cifrada con el sitio se construye dentro del túnel. El proxy ve el nombre del host, no el contenido de la página.
  5. Chrome guarda las credenciales en la caché de autenticación del contexto y las envía de inmediato en las conexiones siguientes.

El paso 5 explica por qué las credenciales acaban perteneciendo al contexto, como muestran las pruebas de más abajo.

¿Cómo se configura un proxy con --proxy-server y page.authenticate()?

npm i puppeteer instala la biblioteca y descarga un Chrome compatible; puppeteer-core es la misma API sin esa descarga. La configuración 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)); // la IP de salida que ve el sitio

await browser.close();

El http:// del flag describe la conexión con el proxy, no las páginas. Un proxy HTTP también lleva los sitios https:// a través del túnel, y su contenido sigue cifrado entre Chrome y el sitio. La referencia de page.authenticate() añade que Puppeteer activa para ello la interceptación de peticiones (request interception) en segundo plano, lo que puede costar algo de rendimiento; pasar null la desactiva.

En nuestras pruebas fallaron tres cosas, y de cada una sale una regla fija:

  • Llama a page.authenticate() antes del primer goto(). Cuando se llamaba después, la primera navegación fallaba con net::ERR_INVALID_AUTH_CREDENTIALS; solo pasaba la siguiente.
  • Deja las credenciales fuera del flag. Con user:pass@ en la dirección, Chrome 154 rechazó el valor completo con net::ERR_NO_SUPPORTED_PROXIES. Las guías antiguas describen aquí un 407; hoy la petición ni siquiera llega al proxy.
  • No envíes Proxy-Authorization por tu cuenta. Al fijar esa cabecera con page.setExtraHTTPHeaders(), Chrome rechazó la navegación con net::ERR_INVALID_ARGUMENT.
Valor de --proxy-serverResultado en Chrome 154
http://pr.proxynet.io:8000Funciona; las páginas HTTPS pasan por un túnel CONNECT
pr.proxynet.io:8000Funciona; sin esquema, Chrome supone un proxy HTTP
http://user:pass@pr.proxynet.io:8000net::ERR_NO_SUPPORTED_PROXIES
socks5://host:portSolo funciona si el servidor no pide contraseña
socks5h://host:portnet::ERR_NO_SUPPORTED_PROXIES; Chrome no conoce este esquema

¿Cómo se usa un proxy distinto en cada contexto de navegador?

Puppeteer 22 cambió el nombre de createIncognitoBrowserContext(), el que todavía usan las guías antiguas, a browser.createBrowserContext(). Sus BrowserContextOptions incluyen dos campos de proxy, proxyServer y proxyBypassList. El navegador puede arrancar sin proxy mientras cada contexto recibe el suyo:

js
import puppeteer from "puppeteer";

const PROXY = "http://pr.proxynet.io:8000";
// Una sesión fija por contexto: mismo ID de sesión -> misma IP de salida durante un máximo de 600 segundos
const sessions = ["a1b2c3", "d4e5f6"];

const browser = await puppeteer.launch(); // el navegador en sí arranca sin 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(); // las cookies, la caché y el ajuste de proxy se van con el contexto
}
await browser.close();

Nuestro proxy de prueba asigna las salidas por ID de sesión, como hace un gateway. Los dos contextos salieron por dos direcciones distintas, y cada uno mantuvo su dirección en la segunda petición. Hay dos resultados más que importan en trabajos reales:

  • Las credenciales pertenecen al contexto, no a la página. Una segunda página del mismo contexto llamó a page.authenticate() con otro usuario, y aun así todas sus peticiones siguieron llevando el primer usuario. Una página que nunca hizo esa llamada pasó con las mismas credenciales guardadas en caché.
  • Un contexto sin proxyServer hereda el proxy del flag de inicio, pero conserva sus propias credenciales. Con --proxy-server en el navegador, dos contextos con sus propios ID de sesión salieron igualmente por dos salidas distintas.
Flag --proxy-servercreateBrowserContext({ proxyServer })
AlcanceTodo el navegadorSolo ese contexto
Salidas separadas en un navegadorSí, un usuario por contextoSí, incluso hosts de proxy distintos
Cambiar de proxyReiniciar el navegadorCerrar el contexto y abrir uno nuevo
Indicado paraUn script, una salidaMuchas sesiones independientes en paralelo

La regla práctica: una sesión, un contexto, un usuario.

¿Sesión rotativa o fija? Cuál le conviene a un navegador

Un navegador no envía una sola petición por página. Una página de producto descarga el documento HTML, scripts, hojas de estilo, imágenes y llamadas a la API en segundo plano, a menudo desde varios hosts y por varias conexiones. En un gateway rotativo cada conexión nueva puede recibir una IP de salida nueva; en nuestra prueba, incluso el favicon de una página salió por una dirección distinta de la de la página. Para páginas sin relación entre sí eso no causa problemas. En un inicio de sesión, un carrito o un formulario de varios pasos, en cambio, el sitio ve a un visitante que salta entre direcciones y puede cerrar la sesión.

El gateway lee el tipo de sesión del nombre de usuario, que el Generador de endpoints del panel escribe por ti. Sus partes se leen con facilidad: -country-de elige el país de salida (la ciudad se elige en el mismo generador), -session-a1b2c3-ttl-600 mantiene una salida para ese ID de sesión durante el número de segundos indicado, entre 1 y 60 minutos, y un usuario sin parte de sesión rota.

Para páginas independientes, como listados de categorías o resultados de búsqueda abiertos en contextos de vida corta, los Proxies rotativos son la opción adecuada.

Para todo lo que guarda estado, usa los Proxies de sesión fija: un contexto, un ID de sesión, mantenido más tiempo del que dura el flujo. Cuando termine el trabajo, cierra el contexto para que las cookies y la IP terminen juntas.

La rotación reparte trabajo independiente entre salidas; no es una forma de saltarse los límites de un sitio. Un 429 Too Many Requests o un 403 Forbidden te pide que vayas más despacio, y una IP nueva no cambia esa respuesta.

¿Funciona Puppeteer con un proxy SOCKS5?

Sí, pero sin usuario ni contraseña. La documentación de proxy de Chromium indica que no se admite ningún método de autenticación para SOCKSv5. Nuestro servidor SOCKS5 local lo vio desde el otro lado: el saludo de Chrome solo ofrecía el método 0x00, «sin autenticación». Un servidor que exigía el método de usuario y contraseña (0x02) tenía que rechazar esa oferta, y la navegación fallaba con net::ERR_SOCKS_CONNECTION_FAILED. page.authenticate() no cambió nada, porque SOCKS5 no tiene un desafío al estilo del 407 al que Puppeteer pueda responder.

La solución es identificarte con tu dirección IP. Añade la IP pública de la máquina donde se ejecuta Chrome a la lista de IP autorizadas del panel (hasta 10 direcciones), toma el puerto SOCKS5 que muestra el Generador de endpoints e inicia el navegador sin credenciales:

js
import puppeteer from "puppeteer";

// Sin usuario ni contraseña: la IP pública de esta máquina debe estar en la lista de IP autorizadas.
// SOCKS5_PORT: el puerto SOCKS5 que muestra el Generador 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();

Chrome envió al servidor SOCKS5 nombres de host, no direcciones IP, así que el DNS se resuelve en el proxy y no hace falta la costumbre de socks5h:// que viene de cURL. Para páginas web, un proxy HTTP suele ser la opción más sencilla, porque funciona con page.authenticate().

¿Cómo se dejan algunas direcciones fuera del proxy?

El flag --proxy-bypass-list, o proxyBypassList como array en un contexto, enumera los hosts a los que Chrome accede directamente. En el flag, las reglas se separan con punto y coma o con comas: --proxy-bypass-list=*.internal.example;192.168.0.0/16. En nuestra prueba, un host de la lista se saltó el proxy por completo y se resolvió en la máquina local.

Chrome tiene además reglas implícitas: localhost, *.localhost, 127.0.0.1/8 y [::1] nunca usan el proxy, aunque la lista esté vacía. Si pruebas contra un servidor local y el registro del proxy no muestra nada, esta es la razón. La regla especial <-loopback> elimina esas reglas implícitas; con ella, nuestra petición a 127.0.0.1 pasó por el proxy.

¿Cómo se comprueba la IP de salida?

Compara, en el mismo navegador, un contexto directo con uno que pasa por el proxy. Si las direcciones son distintas, el tráfico va por el 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();

Si apuntas a un país, consulta también la IP de salida en un servicio de geolocalización. Cuando la comprobación falla, saca a Chrome de la ecuación: ¿Funciona tu proxy? Cómo probar un proxy reúne las pruebas de línea de comandos que distinguen un problema de red de un problema de código.

¿Qué errores vas a ver y qué significan?

Provocamos cada uno de ellos en el entorno de prueba local:

Lo que vesQué pasóQué hacer
net::ERR_PROXY_CONNECTION_FAILEDChrome no pudo llegar al proxy: host o puerto incorrectos, o el tráfico saliente está bloqueadoRevisa la dirección y el puerto; reintentar no ayuda
net::ERR_INVALID_AUTH_CREDENTIALSEl proxy pidió credenciales y Chrome no tenía ningunaLlama a page.authenticate() antes del primer goto()
goto() se resuelve con estado 407La contraseña era incorrecta; Puppeteer lo intentó una vez y se rindióRevisa response.status() y corrige las credenciales
net::ERR_TUNNEL_CONNECTION_FAILEDEl inicio de sesión funcionó, pero el proxy no pudo abrir el túnel hacia el sitioRevisa la dirección de destino
net::ERR_NO_SUPPORTED_PROXIESChrome rechazó el valor de --proxy-serverQuita user:pass@, usa http:// o socks5://
net::ERR_SOCKS_CONNECTION_FAILEDEl servidor SOCKS5 quiere contraseña o tu IP no está autorizadaAutoriza tu IP o usa el puerto HTTP
La página carga, pero la IP es la tuyaUna regla de bypass o un host loopback se saltó el proxyRevisa la lista de bypass y <-loopback>

La fila de la contraseña incorrecta es la que más tarda en descubrirse: nada lanza una excepción y el script sigue adelante con la página 407 del proxy. En un navegador de escritorio, el error de túnel tiene más causas, desde el análisis HTTPS de un antivirus hasta un proxy del sistema que quedó configurado; las tienes en Solucionar err_tunnel_connection_failed en Chrome y Edge. Mientras desarrollas, dos listeners muestran el error real que hay detrás de un tiempo de espera agotado:

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

Ejemplo completo: contextos en paralelo, sesiones fijas y reintentos

El script abre seis páginas generadas con JavaScript con un máximo de tres contextos a la vez. Cada intento recibe un contexto nuevo y una sesión fija nueva, de modo que las cookies y la IP de salida cambian juntas. Las imágenes, los medios y las fuentes se bloquean para ahorrar tráfico; page.setRequestInterception() lo hace sin romper page.authenticate(). Los tiempos de espera agotados y los fallos de túnel se reintentan con backoff exponencial, mientras que los errores de configuración y un 403 o 429 del sitio detienen el trabajo.

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 abiertos al mismo tiempo
const ATTEMPTS = 3;
const BLOCKED = new Set(["image", "media", "font"]);
// Errores de configuración: esperar y reintentar no los arregla
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++) {
    // Un contexto por intento: sus propias cookies y su propia sesión fija (una IP de salida)
    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) {
        // El sitio te rechaza o te frena: una IP nueva no es la respuesta
        throw Object.assign(new Error(`HTTP ${status}: detén el trabajo y baja la frecuencia de peticiones`), { fatal: true });
      }
      if (status >= 400) throw new Error(`HTTP ${status}`);

      await page.waitForSelector("div.quote span.text", { timeout: 10_000 }); // generado con 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 del siguiente intento
    } 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} citas`);
      } catch (err) {
        results.set(url, `ERROR ${err.message.split("\n")[0]}`);
        if (err.fatal) queue.length = 0; // detiene todo el trabajo, no solo esta página
      }
    }
  }),
);
await browser.close();
for (const url of URLS) console.log(url, results.get(url) ?? "omitida");

A través del proxy de prueba local, las seis páginas volvieron con diez citas cada una en menos de cinco segundos. Con el puerto del proxy mal escrito a propósito, las tres páginas en curso se detuvieron al instante con net::ERR_PROXY_CONNECTION_FAILED y las otras tres quedaron omitidas; una página de prueba local que respondía 429 terminó el trabajo de la misma forma. Empieza con un valor bajo de CONCURRENCY: cada contexto ocupa memoria, y el sitio de destino también tiene límites.

Casos de uso

  • Páginas de catálogo y de precios generadas con JavaScript que un cliente HTTP simple ve vacías.
  • Comprobar cómo se ven tu propio sitio, tus precios o tus banners de consentimiento desde otro país, con un contexto por país.
  • Capturas de pantalla y PDF de páginas públicas tal como las ve un visitante de un país concreto.
  • Supervisar tus propias páginas y flujos de pago desde fuera de tu red.

El mismo marco vale para todos: respeta el robots.txt y las condiciones del sitio, usa una API oficial cuando exista y mantén la frecuencia de peticiones en un nivel que el sitio pueda soportar.

Errores frecuentes

  • Escribir user:pass@ en --proxy-server. Chrome rechaza el valor con net::ERR_NO_SUPPORTED_PROXIES.
  • Esperar un usuario nuevo por página. Chrome conserva las primeras credenciales para todo el contexto; usa un contexto por sesión.
  • Intentar una contraseña con SOCKS5. Chrome solo ofrece «sin autenticación»; usa la lista de IP autorizadas o el puerto HTTP.
  • Probar contra localhost y fiarse del resultado. Las direcciones loopback se saltan el proxy salvo que añadas <-loopback>.
  • Tratar una respuesta 407 como si fuera una página. Una contraseña incorrecta no lanza excepción; revisa response.status().
  • Rotar la IP tras un 429 o un 403. Ve más despacio; el límite tiene que ver con tu comportamiento, no con tu dirección.
  • Dejar contextos abiertos. Cada uno ocupa memoria; ciérralos en un bloque finally.

Guía de decisión

NecesidadRecomendación
Un script, una salidaFlag --proxy-server más page.authenticate()
Muchas sesiones independientes en un navegadorcreateBrowserContext({ proxyServer }), un ID de sesión por contexto
Muchas páginas que no dependen entre síUsuario rotativo, contextos de vida corta
Inicio de sesión, carrito o formulario de varios pasosSesión fija más larga que el flujo, un contexto
SOCKS5 es obligatorioLista de IP autorizadas, sin credenciales
Una herramienta que solo acepta el flag de inicioLista de IP autorizadas o un reenviador local como proxy-chain
Los hosts internos deben ir directos--proxy-bypass-list o proxyBypassList

Preguntas frecuentes

¿Tiene puppeteer.launch() una opción de proxy?

No. El proxy va en args como el flag --proxy-server de Chrome, y el inicio de sesión en page.authenticate(). La API propia de Puppeteer solo tiene campos de proxy en browser.createBrowserContext(): proxyServer y proxyBypassList.

¿Puede cada página tener su propio proxy?

No dentro de un mismo contexto. El proxy y las credenciales en caché pertenecen al contexto, así que en nuestra prueba una segunda página con otras credenciales siguió usando las primeras. Da a cada página que necesite su propia salida su propio contexto; un contexto cuesta mucho menos que un navegador nuevo.

¿Qué es proxy-chain y lo necesitas?

proxy-chain es un paquete de código abierto para Node.js cuya función anonymizeProxy() inicia un proxy local sin contraseña y reenvía el tráfico a un proxy upstream con credenciales, de modo que Chrome recibe una dirección simple http://127.0.0.1:<port>. En nuestra prueba funcionó tanto con un upstream HTTP como con uno SOCKS5 que exigían contraseña. Con page.authenticate() o una lista de IP autorizadas no lo necesitas.

¿Se puede cambiar de proxy sin reiniciar el navegador?

Sí, mediante contextos. Cierra el contexto, abre uno nuevo con otro proxyServer u otro ID de sesión y vuelve a llamar a page.authenticate(). Solo el flag --proxy-server queda fijo durante toda la vida del navegador.

¿Por qué el registro del proxy muestra peticiones que no hiciste?

El tráfico de fondo del propio Chrome, como las comprobaciones de actualizaciones y las peticiones a servicios de Google, va al mismo proxy. En nuestro registro, la mayor parte llegó sin credenciales y recibió un 407, porque page.authenticate() solo responde a los desafíos del tráfico de las páginas. No afecta a tus páginas.

¿Un proxy evita los CAPTCHA en Puppeteer?

No. Un proxy cambia de dónde viene el tráfico, mientras que los CAPTCHA también reaccionan a la frecuencia de peticiones, a las señales del navegador y a las sesiones incoherentes. Las causas y las formas legítimas de reducirlos están en Puppeteer y CAPTCHA: por qué aparece y cómo reducirlo.

En resumen

En Puppeteer el proxy es un ajuste de Chrome: el flag --proxy-server para todo el navegador o proxyServer para un contexto, con el inicio de sesión en page.authenticate() antes de la primera navegación. Las credenciales se guardan en caché por contexto, así que monta cada sesión como un contexto, elige sesión fija o rotativa a través del usuario y usa la lista de IP autorizadas para SOCKS5. Revisa response.status() y mantén una frecuencia de peticiones razonable. Para páginas que deben abrirse desde conexiones domésticas normales, las salidas vienen de un pool de Proxies residenciales.