Los frameworks de automatización de navegador aceptan un proxy como opción de lanzamiento, pero los proxies autenticados requieren un manejo especial porque Chromium no acepta user:pass@ en una URL de proxy: Playwright toma las credenciales como campos separados (y puede establecer un proxy diferente por contexto de navegador), Puppeteer necesita page.authenticate(), y Selenium necesita una extensión o un forwarder local. Esta guía ofrece código funcional para cada uno, y luego cubre la parte que importa más que la sintaxis: un navegador headless detrás de una IP residencial sigue siendo identificable a menos que el resto de su historia coincida.

El dato que lo condiciona todo: Chromium ignora las credenciales de proxy en la URL

Las herramientas de línea de comandos y las librerías de Python aceptan sin problema http://user:pass@host:port. El flag --proxy-server de Chromium solo acepta host:port (con esquema opcional); las credenciales incluidas se descartan silenciosamente. Cuando el proxy responde con 407, el navegador muestra un diálogo de autenticación — que ningún script de automatización está observando. Por eso cada framework tiene su propia forma de proporcionar el nombre de usuario y la contraseña, y ahí es donde surgen la mayoría de los problemas de “el proxy funciona en curl pero no en mi script de navegador”. Firefox es más permisivo en algunos caminos, pero los mecanismos específicos de cada framework que se muestran a continuación funcionan en todos los motores.

Playwright

Playwright tiene soporte de proxy de primera clase con campos de credenciales separados, y — la característica que lo convierte en la mejor opción para trabajos con múltiples identidades — el proxy se puede establecer por contexto de navegador, de modo que un solo proceso de navegador puede alojar varias sesiones aisladas, cada una saliendo desde una IP diferente.

Python

from playwright.sync_api import sync_playwright

PROXY = {"server": "http://proxy.example.com:8080",
         "username": "user", "password": "pass"}

with sync_playwright() as p:
    # opción A: un proxy para todo el navegador
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()
    page.goto("https://api.ipify.org?format=json")
    print(page.text_content("body"))
    browser.close()

    # opción B: proxy diferente por contexto (un navegador, muchas identidades)
    browser = p.chromium.launch(proxy={"server": "per-context"})   # marcador de posición; los proxies reales vienen de los contextos
    ctx_de = browser.new_context(proxy={"server": "http://gate.provider.com:7777",
                                        "username": "customer-country-de-session-a1", "password": "pass"},
                                 locale="de-DE", timezone_id="Europe/Berlin")
    ctx_us = browser.new_context(proxy={"server": "http://gate.provider.com:7777",
                                        "username": "customer-country-us-session-b2", "password": "pass"},
                                 locale="en-US", timezone_id="America/New_York")
    for ctx in (ctx_de, ctx_us):
        pg = ctx.new_page(); pg.goto("https://api.ipify.org?format=json"); print(pg.text_content("body"))
    browser.close()

Dos detalles. Los proxies por contexto en Chromium requieren que el navegador se haya lanzado con una opción de proxy (cualquier valor — el marcador de posición documentado es {"server": "per-context"}); sin él, los contextos no pueden anularlo. Y nota que locale y timezone_id se establecen junto al proxy: una IP alemana con una zona horaria estadounidense es una contradicción que los sistemas anti-bot ponderan, como se explica en cómo funcionan los sistemas anti-bot. Los proxies SOCKS5 son compatibles ("server": "socks5://host:1080"), pero Chromium no envía credenciales a los proxies SOCKS, así que usa autenticación por lista blanca de IP con SOCKS.

Node.js

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ proxy: { server: 'per-context' } });
  const context = await browser.newContext({
    proxy: { server: 'http://gate.provider.com:7777',
             username: 'customer-country-de-session-a1', password: 'pass' },
    locale: 'de-DE', timezoneId: 'Europe/Berlin',
  });
  const page = await context.newPage();
  await page.goto('https://api.ipify.org?format=json');
  console.log(await page.textContent('body'));
  await browser.close();
})();

El campo bypass de Playwright ("bypass": "localhost,*.internal") excluye hosts del proxy, y launch_persistent_context() acepta la misma opción proxy si necesitas un directorio de perfil persistente.

Puppeteer

Puppeteer pasa el proxy como un flag de Chromium y autentica por página:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--proxy-server=http://proxy.example.com:8080'],
  });
  const page = await browser.newPage();
  await page.authenticate({ username: 'user', password: 'pass' });   // responde al 407 para esta página
  await page.goto('https://api.ipify.org?format=json');
  console.log(await page.evaluate(() => document.body.innerText));
  await browser.close();
})();

page.authenticate() debe llamarse antes de la navegación y en cada página (pestaña) que vaya a usar el proxy. El proxy es a nivel de navegador — Puppeteer no tiene una configuración de proxy por contexto como Playwright. Para varias identidades tienes tres opciones: lanzar varios procesos de navegador (un proxy cada uno; pesado pero simple), usar --proxy-server apuntando a un proxy de conmutación local que enrute según algún marcador, o pasarte a Playwright. También es útil: --proxy-bypass-list=<-loopback> si necesitas que localhost también pase por el proxy, y puppeteer-extra con su plugin de proxy, que maneja la autenticación y puede rotar por página.

Selenium

Selenium establece el proxy a través de las opciones del navegador. Los proxies sin autenticación (con lista blanca de IP) son una línea; los autenticados son el caso incómodo.

Chrome, autenticación por lista blanca

from selenium import webdriver

opts = webdriver.ChromeOptions()
opts.add_argument("--proxy-server=http://proxy.example.com:8080")
driver = webdriver.Chrome(options=opts)
driver.get("https://api.ipify.org?format=json")
print(driver.find_element("tag name", "body").text)
driver.quit()

Chrome, nombre de usuario/contraseña

El protocolo WebDriver de Selenium no tiene un gancho para responder al 407 del proxy. Los workarounds estándar:

  1. Selenium Wire (Python): una extensión de Selenium que enruta el tráfico a través de su propio proxy local y soporta upstreams autenticados:
    from seleniumwire import webdriver   # pip install selenium-wire
    opts = {"proxy": {"http": "http://user:pass@proxy.example.com:8080",
                      "https": "http://user:pass@proxy.example.com:8080"}}
    driver = webdriver.Chrome(seleniumwire_options=opts)
    Ten en cuenta que Selenium Wire ya no se mantiene activamente; todavía funciona con Chrome actual para muchos, pero trátalo como un parche temporal.
  2. Una extensión de Chrome generada que establece el proxy mediante chrome.proxy y responde a onAuthRequired con las credenciales — un patrón bien conocido; la extensión es un manifest más unas pocas líneas de script de fondo, cargada con opts.add_extension(...). No funciona en modo headless con Chrome antiguo (las extensiones no eran compatibles allí); funciona en el headless=new actual.
  3. Un proxy de reenvío local en 127.0.0.1 que contenga las credenciales y reenvíe al upstream; apunta Chrome a él sin autenticación. Independiente del framework y robusto.
  4. Autenticación por lista blanca del proveedor, si tu máquina tiene una IP estática — evita el problema por completo.

Firefox

opts = webdriver.FirefoxOptions()
opts.set_preference("network.proxy.type", 1)
opts.set_preference("network.proxy.http", "proxy.example.com");  opts.set_preference("network.proxy.http_port", 8080)
opts.set_preference("network.proxy.ssl",  "proxy.example.com");  opts.set_preference("network.proxy.ssl_port", 8080)
opts.set_preference("network.proxy.socks_remote_dns", True)     # DNS remoto si se usa SOCKS
driver = webdriver.Firefox(options=opts)

Firefox aún mostrará un diálogo de autenticación para proxies con nombre de usuario/contraseña; aplican los enfoques de forwarder local o lista blanca.

Lo que el proxy no soluciona en un navegador headless

Un navegador real te hace pasar la capa TLS y la huella de HTTP/2 — el handshake es genuinamente de Chrome (ver huella TLS). Pero no te hace pasar, por sí solo, la capa de JavaScript:

  • navigator.webdriver es true bajo automatización; los sitios lo comprueban primero.
  • El modo headless tiene diferencias de renderizado y API (plugins ausentes, cadenas específicas del renderizador WebGL, dimensiones de ventana) que los scripts de huella prueban. El headless más nuevo de Chrome (headless=new) está mucho más cerca del con interfaz, pero no es idéntico.
  • WebRTC puede seguir exponiendo la IP real de la máquina a menos que se desactive o se falsifique; un proxy no lo toca. Pruébalo — Cómo probar un proxy.
  • Zona horaria, locale y geolocalización provienen de la máquina a menos que los establezcas (Playwright: timezone_id, locale, geolocation); un desajuste con el país del proxy es una señal clásica.
  • Comportamiento: navegaciones instantáneas, sin movimiento del ratón, tiempos idénticos en cada ejecución.

Los plugins de sigilo (puppeteer-extra-plugin-stealth, playwright-stealth) parchean los artefactos más obvios; los navegadores antidetect van más allá proporcionando una huella coherente por perfil y exponiendo un puerto de depuración al que se conecta tu script de Playwright/Puppeteer — que es la respuesta de producción para trabajo con cuentas. Ver qué es un navegador antidetect y, para elegir el tipo de IP detrás de cada perfil, nuestro ranking de proxies para antidetect.

Elegir el framework para trabajar con proxies

PlaywrightPuppeteerSelenium
Proxy autenticadoNativo (campos username/password)page.authenticate() por páginaSe necesita workaround (Wire, extensión, forwarder local)
Proxy diferente por sesiónPor contexto, un navegadorUn navegador por proxyUn driver por proxy
SOCKS5Sí (sin credenciales → lista blanca)Mediante flag (sin credenciales)Mediante opciones/preferencias
Locale / zona horaria por sesiónOpciones de contexto nativasAPIs de emulación / flagsPreferencias / CDP
Mejor paraScraping y pruebas con múltiples identidadesAutomatización de Chrome centrada en NodeSuites heredadas, grids de navegadores cruzados

Preguntas frecuentes

¿Cómo uso un proxy autenticado en Playwright?

Pasa proxy={"server": "http://host:port", "username": "…", "password": "…"} a launch() o, por contexto, a new_context() (primero lanza con proxy={"server": "per-context"}).

¿Cómo uso un proxy con autenticación en Puppeteer?

Lanza con args: ['--proxy-server=host:port'] y llama a await page.authenticate({username, password}) antes de page.goto() en cada página.

¿Por qué Chrome ignora user:pass@ en --proxy-server?

El flag de Chromium solo acepta esquema://host:port; las credenciales no se analizan. Responde al 407 del proxy con un diálogo interactivo, por lo que la automatización debe proporcionar las credenciales a través del mecanismo del framework.

¿Puedo usar un proxy diferente para cada pestaña?

No por pestaña. Playwright lo hace por contexto de navegador (cada contexto es una sesión aislada); Puppeteer y Selenium necesitan una instancia de navegador por proxy, o un proxy de conmutación local.

¿Un proxy hace que Chrome headless sea indetectable?

No. Arregla la capa de IP y, al ser un navegador real, la capa TLS. Los flags de automatización, los artefactos headless, WebRTC, el desajuste de zona horaria y el comportamiento son señales separadas que requieren un manejo separado.

¿Debería ser residencial o de datacenter el proxy para automatización de navegador?

Lo que requiera la política de IP del objetivo — el navegador no cambia ese cálculo. Datacenter donde se tolere; residencial o móvil en sitios protegidos; estático (ISP) para perfiles persistentes con sesión iniciada. Ver la comparación de tipos de proxy.