Volver al Centro de conocimiento
Primeros pasos

Proxy Playwright: rutas, autenticación y señales

Diagnostica rutas, exclusiones de contexto y autenticación SOCKS5 en Playwright; distingue dos salidas reales de las señales del perfil que permanecen iguales.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Primeros pasos?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

Un proxy de Playwright puede enviar una petición por la salida prevista mientras la zona horaria, el idioma y el canvas permanecen iguales. Comprueba por separado la ruta y el entorno del navegador: un proxy operativo no define un perfil nuevo. BotBrowser admite entradas declaradas de perfil, proxy, zona horaria, configuración regional e idioma, pero no puede hacer que una ruta ascendente acepte credenciales ni garantizar cómo interpretará esas entradas el destino.

TL;DR

  • Demuestra la ruta de la petición con un origen u observador propio antes de cambiar los ajustes del navegador. Que una página cargue correctamente no determina qué proxy la gestionó.
  • Separa los valores predeterminados del lanzamiento del proxy explícito de un contexto. Las exclusiones de ruta seleccionan una conexión directa; no recurren a otro proxy como alternativa.
  • Las credenciales HTTP documentadas por Playwright no proporcionan a Chromium compatibilidad nativa con contraseñas SOCKS5. Comprueba el límite del protocolo antes de investigar una página de inicio de sesión.
  • Compara copias idénticas byte a byte de un perfil cerrado a través de dos salidas públicas validadas. Los valores estables del navegador son observaciones, no pruebas de cómo decide el destino la identidad.

Contenido

Empieza por la operación que falló

Conserva la primera operación fallida y su respuesta. Un fallo al crear un contexto no es lo mismo que un proxy rechazando una conexión; ambos ocurren antes de que un sitio web rechace una acción de cuenta. Para diagnosticar playwright proxy, el registro inicial más útil contiene el esquema del proxy, el ámbito de la configuración, el esquema del destino y el primer error. Sustituye las credenciales por una etiqueta antes de compartir registros: un mensaje de error puede incluir la dirección del servidor o una URL con secretos incorporados.

ObservaciónEvidencia que recopilarPrimer cambio controlado
La creación del contexto rechaza las credenciales SOCKS5Error de API antes de navegar; cero conexiones al listenerComprobar la compatibilidad documentada del protocolo
El proxy HTTP solicita autenticación con 407Contadores de solicitudes de autenticación y reenvíos aceptadosComprobar las credenciales HTTP de ese proxy
Un origen devuelve 401Respuesta del origen, separada del desafío del proxyExaminar la autenticación de ese origen
La página carga por una ruta inesperadaContadores de los proxies de lanzamiento y contexto, y marcador del origenCorregir el ámbito de configuración o la exclusión
Ambas rutas muestran la misma dirección públicaRespuestas del observador, marcas de tiempo y etiquetas de ruta del proveedorValidar la ruta ascendente antes de comparar señales
Las salidas difieren, pero los valores del navegador coincidenInstantánea del perfil, binario y lecturas individuales de la páginaExaminar la entrada del navegador relevante para el requisito

Un tiempo de espera agotado al navegar no constituye un diagnóstico. Aumentar el tiempo de espera de page.goto() puede ayudar con un endpoint autorizado lento, pero no puede añadir un método de autenticación de transporte ni cambiar la ruta de un contexto. Del mismo modo, un 403 del destino no identifica el componente que falló sin más evidencia. Conserva la respuesta y respeta el límite autorizado del diagnóstico, en lugar de cambiar repetidamente ajustes sin relación contra un tercero.

La URL del proxy y la URL del destino responden a preguntas distintas. http://proxy.test:8080 designa una conexión a un proxy HTTP. Un destino https:// puede viajar por su túnel CONNECT sin convertir al propio proxy en un proxy HTTPS. Registra ambos esquemas. La aceptación de un túnel tampoco certifica qué salida seleccionó el proveedor ascendente. Las diferencias entre fallos HTTP, HTTPS y SOCKS5 en Puppeteer cubren ese vocabulario de protocolos; la configuración separada de lanzamiento y contexto de Playwright añade el problema de ámbito siguiente.

Utiliza un origen que devuelva un marcador inequívoco, con un presupuesto de peticiones y un contador. El origen demuestra que llegó la respuesta prevista; el contador del proxy demuestra qué ruta local la reenvió. Un observador de IP responde a otra pregunta: la dirección pública visible en esa petición. Mantener separados estos tres registros evita tratar una captura de página como evidencia de red. Tanto las peticiones directas como las reenviadas pueden mostrar el mismo HTML del origen.

Separa las rutas de lanzamiento, de contexto y las exclusiones directas

La API documentada acepta un proxy al lanzar el navegador y al crear un contexto. Un lanzamiento persistente también acepta ajustes de proxy junto con su userDataDir. Estas configuraciones tienen duraciones distintas. Un browser.newContext() normal está aislado y no escribe su estado de navegación en ese directorio. Un contexto persistente utiliza estado almacenado en disco; cerrarlo cierra también su navegador asociado. Elegir uno porque un ejemplo lo utiliza puede cambiar las condiciones de almacenamiento del experimento.

Un proxy explícito de contexto es una nueva elección de ruta, no una orden de probar primero el proxy de lanzamiento y usar el del contexto si falla. Trata toda la configuración del contexto como una unidad: servidor, credenciales compatibles y lista de exclusiones. Comprueba cada campo donde se crea ese contexto. Una ruta montada en launch() no determina lo que suministró un newContext({ proxy }) posterior. Las decisiones de configuración del navegador con Playwright explican la propiedad del navegador y la duración del contexto antes de iniciar la comparación de red.

El miembro exacto proxy.bypass designa la lista documentada de exclusiones de ruta separadas por comas. Aquí, una exclusión significa que una petición coincidente se conecta directamente en lugar de pasar por ese proxy configurado. No significa que el destino conceda acceso ni define una identidad del navegador. Que una petición quede excluida del proxy de contexto C no demuestra que la gestionara el proxy de lanzamiento L. La matriz controlada necesita contadores L y C separados para distinguir una llegada directa al origen de cualquiera de las rutas reenviadas.

Los contadores de ruta determinan el trayecto de red elegido; las lecturas de un perfil inmutable solo establecen los valores de página medidos.

El navegador interpreta finalmente sus reglas de ruta, así que comprueba cada regla con la forma exacta de URL que utiliza la carga de trabajo. Chromium distingue un nombre de host simple, un patrón de dominio y una URL con IP literal. Su regla <local> se refiere a nombres de host simples sin punto; no es sinónimo de toda dirección que termina resolviéndose a una máquina local. Una regla CIDR se aplica a una IP literal en la URL, no a un nombre de host solo porque el DNS devuelva una dirección de esa subred. Conserva la escritura del host y el puerto en el registro del fallo.

El comportamiento de loopback merece un caso local explícito. Chromium tiene exclusiones implícitas para direcciones locales, mientras Playwright también construye reglas de ruta de loopback. La implementación 1.63.0 comprueba las entradas explícitas de loopback antes de decidir si añade su modificador. Un resultado anterior no puede trasladarse solo porque la opción conserve el mismo nombre. Ni afirmar de forma general que localhost siempre conecta directamente ni suponer un ámbito sin medirlo constituye evidencia suficiente. Comprueba los contadores de reenvío en 127.0.0.1, registrando la versión instalada; no hacen falta variables de entorno no documentadas ni opciones adicionales del resolvedor.

La referencia de configuración de proxy de BotBrowser define las entradas de red del producto. Esas entradas no sustituyen el ámbito de objetos ni las reglas de combinación de Playwright. Al cambiar de biblioteca de automatización, adapta deliberadamente el modelo de configuración en lugar de copiar un objeto de aspecto parecido y suponer que conserva la misma semántica de ruta.

Reproduce la ruta antes de cambiar el navegador

Un fixture local del fallo elimina el comportamiento del destino del diagnóstico. Inicia un origen HTTP propio en un puerto de loopback asignado dinámicamente y un proxy que solo reenvíe a ese origen. Utiliza un marcador único de respuesta y registra los desafíos de autenticación, los reenvíos aceptados y las peticiones directas al origen. El proxy debe rechazar todos los demás destinos. Un reenviador abierto de uso general no es necesario para reproducir un error de credenciales HTTP.

Para la autenticación HTTP, ejecuta como casos separados credenciales correctas, ausentes e incorrectas. El caso satisfactorio debe demostrar tanto el reenvío como el marcador del origen. Los casos rechazados deben demostrar que no se alcanzó el origen, en lugar de limitarse a coincidir con una cadena de error conveniente. Los reintentos del navegador pueden producir más de un desafío, por lo que conviene afirmar la propiedad relevante, como cero reenvíos aceptados, y no suponer un número universal de intentos de socket. No trates estos resultados HTTP locales como una prueba de la política de autenticación de un proveedor ascendente.

SOCKS5 necesita su propio límite. La implementación fijada de Playwright 1.63.0 rechaza las opciones de usuario y contraseña del proxy para la ruta SOCKS5 de Chromium. En ese caso, el fallo puede producirse antes de que el listener SOCKS reciba una conexión. No es un fallo de inicio de sesión del sitio web, y mover las credenciales a cabeceras HTTP del origen no añade la negociación SOCKS ausente. Un listener SOCKS sin autenticación y un intento de configuración con credenciales comprueban afirmaciones distintas. Un adaptador local que se autentica ante un servicio SOCKS ascendente es otra arquitectura y debe etiquetarse como tal.

El ámbito de la ruta también necesita casos independientes. Asigna contadores distintos al proxy de lanzamiento L y al proxy de contexto C. Establece primero el valor predeterminado de lanzamiento, después crea un contexto con una ruta C explícita y, por último, comprueba una exclusión directa en ese contexto C. Mantén el mismo marcador del origen. De lo contrario, una página distinta puede ocultar una ruta distinta y dejar sin aclarar qué modificación causó la aparente solución. El contador del origen por sí solo no distingue L de C, porque ambos proxies pueden reenviar el mismo contenido.

Los trabajos del navegador deben utilizar un pool acotado, en lugar de ejecutarse en serie mientras cambian las condiciones de la máquina. El fixture acepta --pool y --workers, solicita 20 por defecto y mide la CPU y la memoria disponibles antes de elegir el paralelismo real. Solicitar más plazas no crea más casos independientes. Si una comparación externa no puede sostener dos ejecuciones simultáneas del navegador, detente antes de hacer las peticiones al observador; ejecutar A ahora y B mucho después debilita la comparación controlada.

Resultados locales observados y un fixture ejecutable

La ejecución controlada utilizó Playwright 1.63.0, Chrome estándar 154.0.8037.57, Node 20.20.2 y Ubuntu 24.04.5 LTS el 11 de noviembre de 2026, hora de Toronto. El fixture HTTP público ejecutó seis trabajos simultáneos: 31.88 núcleos de CPU estimados como inactivos y 121726328832 bytes de memoria disponible permitieron los seis. El pool solicitado fue 20, limitado por el número de casos y no por un límite serial arbitrario. Los tres casos SOCKS separados también se ejecutaron simultáneamente. Este entorno aislado con origen propio, ejecutado como root, usó el valor predeterminado de Playwright chromiumSandbox: false; esa condición no es una recomendación para desplegar en producción.

Caso controladoResultado observadoContador que demuestra el resultado
HTTP de lanzamiento, credenciales correctasMarcador del origen a través de LL reenviadas 1; C reenviadas 0
HTTP de lanzamiento, credenciales incorrectasSin marcador del origenL desafíos 2; reenviadas 0
HTTP de lanzamiento, credenciales ausentesSin marcador del origenL desafíos 1; reenviadas 0
El lanzamiento excluye explícitamente 127.0.0.1Marcador directo del origenOrigen directo 1; L/C reenviadas 0
El contexto C sustituye al lanzamiento L excluidoMarcador del origen a través de CC reenviadas 1; L reenviadas 0
El contexto C excluye explícitamente 127.0.0.1Marcador directo del origenOrigen directo 1; L/C reenviadas 0
SOCKS5 sin credencialesMarcador del origenEnrutadas 1; llegadas al origen 1
Credenciales SOCKS5 en el lanzamientoRechazo de la APIConexiones al listener 0; llegadas al origen 0
Credenciales SOCKS5 en el ámbito del contextoRechazo de la APIConexiones al listener 0; llegadas al origen 0

Son recuentos medidos, no números universales de reintentos. Las credenciales HTTP incorrectas produjeron net::ERR_HTTP_RESPONSE_CODE_FAILURE; las ausentes produjeron net::ERR_INVALID_AUTH_CREDENTIALS. Ambos casos de credenciales SOCKS devolvieron Browser does not support socks5 proxy authentication. Los proxies locales también rechazaron intentos de conexión de fondo sin relación. Solo la petición de aplicación con el marcador único cuenta como reenvío satisfactorio. El resultado de loopback de 1.63.0 coincide con su implementación fijada; un resultado exploratorio anterior de 1.49.0 fue distinto y no sustituye esta ejecución.

Guarda el siguiente ejemplo completo, exclusivamente local, como playwright-local-proxy.mjs junto a un paquete [email protected] instalado. Asigna a CHROME un ejecutable de Chrome estándar. El host Linux probado proporcionaba DISPLAY=:88; su navegador se ejecutó en modo headless. No hacen falta credenciales ascendentes ni un destino externo. El script rechaza destinos de proxy sin relación, gestiona la limpieza, comprueba CPU y memoria, y termina con error si fallan las aserciones o hay menos de dos plazas disponibles.

CHROME=/opt/google/chrome/chrome node playwright-local-proxy.mjs --pool 20
import assert from 'node:assert/strict';
import { randomBytes } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';

const require = createRequire(process.env.PLAYWRIGHT_RESOLVE_FROM || import.meta.url);
const { chromium } = require('playwright');
const playwrightVersion = require('playwright/package.json').version;
const expectedVersion = process.env.EXPECTED_PLAYWRIGHT_VERSION || '1.63.0';
assert.equal(playwrightVersion, expectedVersion, `Install playwright@${expectedVersion} beside this script`);
const CHROME = process.env.CHROME;
const flags = [
  '--password-store=basic',
  '--use-mock-keychain',
  '--disable-background-networking',
  '--disable-component-update',
  '--disable-sync',
  '--no-first-run',
  '--no-default-browser-check',
];
const safeError = error => ({
  name: error.name,
  networkCode: String(error.message).match(/net::ERR_[A-Z_]+/)?.[0] || null,
});
export async function resources() {
  const sample = () =>
    os.cpus().reduce(
      (sum, cpu) => ({
        idle: sum.idle + cpu.times.idle,
        total: sum.total + Object.values(cpu.times).reduce((a, b) => a + b, 0),
      }),
      { idle: 0, total: 0 }
    );
  const before = sample();
  await new Promise(resolve => setTimeout(resolve, 250));
  const after = sample();
  const unusedCpuCores = (os.cpus().length * (after.idle - before.idle)) / Math.max(1, after.total - before.total);
  let availableMemoryBytes = os.freemem();
  if (process.platform === 'darwin') {
    const vm = execFileSync('vm_stat', { encoding: 'utf8' });
    const pageSize = Number(vm.match(/page size of (\d+) bytes/)[1]);
    availableMemoryBytes = [...vm.matchAll(/^(?:Pages free|Pages inactive|Pages speculative):\s+(\d+)\./gm)].reduce(
      (sum, match) => sum + Number(match[1]) * pageSize,
      0
    );
  } else if (process.platform === 'linux') {
    availableMemoryBytes =
      Number((await fs.readFile('/proc/meminfo', 'utf8')).match(/^MemAvailable:\s+(\d+)/m)[1]) * 1024;
  }
  return {
    capturedAt: new Date().toISOString(),
    cpuCount: os.cpus().length,
    unusedCpuCores: Number(unusedCpuCores.toFixed(2)),
    availableMemoryBytes,
  };
}
export function requestedPool(argv = process.argv.slice(2)) {
  const index = argv.findIndex(value => /^--?(pool|workers)$/i.test(value));
  const requested = index < 0 ? 20 : Number(argv[index + 1]);
  assert.ok(Number.isInteger(requested) && requested > 0, 'Pool must be a positive integer');
  return requested;
}
export function capacity(snapshot, requested, jobs) {
  const cpuSlots = Math.floor(snapshot.unusedCpuCores);
  const memorySlots = Math.floor(snapshot.availableMemoryBytes / 1024 ** 3);
  return {
    requested,
    jobs,
    effective: Math.min(requested, jobs, Math.max(0, cpuSlots), Math.max(0, memorySlots)),
    cpuSlots,
    memorySlots,
    reason: 'One estimated idle CPU core and 1 GiB per browser; at least two concurrent slots required',
  };
}
export async function pool(jobs, size) {
  let next = 0;
  const results = [];
  await Promise.all(
    Array.from({ length: size }, async () => {
      while (next < jobs.length) {
        const index = next++;
        results[index] = await jobs[index]();
      }
    })
  );
  return results;
}
export async function listen(server) {
  const sockets = new Set();
  server.on('connection', socket => {
    sockets.add(socket);
    socket.on('close', () => sockets.delete(socket));
    socket.on('error', () => {});
  });
  await new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', resolve);
  });
  return {
    port: server.address().port,
    async close() {
      for (const socket of sockets) socket.destroy();
      await new Promise(resolve => server.close(resolve));
    },
  };
}
export async function httpCase(spec) {
  let origin, left, right, browser;
  const marker = randomBytes(12).toString('hex');
  const counts = { origin: { direct: 0, L: 0, C: 0 }, originOtherRequests: 0 };
  const result = { case: spec.name, startedAt: new Date().toISOString(), expectedRoute: spec.expected };
  try {
    origin = await listen(
      http.createServer((req, res) => {
        if (req.url !== '/') {
          counts.originOtherRequests++;
          res.writeHead(404).end();
          return;
        }
        const route = req.headers['x-local-route'] || 'direct';
        counts.origin[route]++;
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(`<p id="marker">${marker}</p><p id="route">${route}</p>`);
      })
    );
    const target = `http://127.0.0.1:${origin.port}/`;
    async function proxyServer(label) {
      const counter = (counts[label] = { requests: 0, challenges: 0, accepted: 0, forwarded: 0, denied: 0 });
      const expected = `Basic ${Buffer.from('local-user:local-password').toString('base64')}`;
      const server = http.createServer((req, res) => {
        if (req.url !== target) {
          counter.denied++;
          res.writeHead(403).end();
          return;
        }
        counter.requests++;
        if (req.headers['proxy-authorization'] !== expected) {
          counter.challenges++;
          res.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="owned-local-test"' }).end();
          return;
        }
        counter.accepted++;
        counter.forwarded++;
        const upstream = http.request(
          {
            host: '127.0.0.1',
            port: origin.port,
            path: '/',
            headers: { Host: `127.0.0.1:${origin.port}`, 'X-Local-Route': label },
          },
          remote => {
            res.writeHead(remote.statusCode, remote.headers);
            remote.pipe(res);
          }
        );
        upstream.on('error', () => {
          if (!res.headersSent) res.writeHead(502);
          res.end();
        });
        req.pipe(upstream);
      });
      server.on('connect', (req, socket) => {
        counter.denied++;
        socket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
      });
      return listen(server);
    }
    left = await proxyServer('L');
    right = await proxyServer('C');
    function settings(port, excluded, credential = 'correct') {
      const proxy = { server: `http://127.0.0.1:${port}` };
      if (credential !== 'absent') {
        proxy.username = 'local-user';
        proxy.password = credential === 'wrong' ? 'incorrect' : 'local-password';
      }
      if (excluded) proxy.bypass = '127.0.0.1';
      return proxy;
    }
    browser = await chromium.launch({
      executablePath: CHROME,
      headless: true,
      proxy: settings(left.port, spec.launchExcluded, spec.credential),
      args: flags,
      timeout: 25000,
    });
    result.browserVersion = browser.version();
    const context = await browser.newContext(
      spec.contextProxy ? { proxy: settings(right.port, spec.contextExcluded) } : {}
    );
    await context.route('**/*', route => (route.request().url() === target ? route.continue() : route.abort()));
    const page = await context.newPage();
    let response = null,
      navigationError = null;
    try {
      response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 12000 });
    } catch (error) {
      navigationError = safeError(error);
    }
    let content = '',
      contentReadError = null;
    try {
      content = await page.content();
    } catch (error) {
      contentReadError = {
        ...safeError(error),
        pageChangingAfterFailure: /Unable to retrieve content/.test(String(error.message)),
      };
    }
    const markerObserved = content.includes(marker);
    const observedRoute = markerObserved ? await page.locator('#route').textContent() : null;
    Object.assign(result, {
      navigationStatus: response?.status() ?? null,
      navigationError,
      contentReadError,
      markerObserved,
      observedRoute,
    });
    assert.equal(observedRoute, spec.expected);
    for (const label of ['direct', 'L', 'C']) assert.equal(counts.origin[label], label === spec.expected ? 1 : 0);
    for (const label of ['L', 'C']) assert.equal(counts[label].forwarded, label === spec.expected ? 1 : 0);
    if (spec.expected) assert.equal(response.status(), 200);
    else {
      assert.ok(counts.L.challenges > 0);
      assert.equal(counts.L.accepted, 0);
    }
    return { ...result, status: 'passed', counts };
  } catch (error) {
    return { ...result, status: 'failed', error: safeError(error), counts };
  } finally {
    if (browser) await browser.close();
    if (left) await left.close();
    if (right) await right.close();
    if (origin) await origin.close();
  }
}
export const cases = [
  { name: 'launch-http-correct', expected: 'L' },
  { name: 'launch-http-wrong', credential: 'wrong', expected: null },
  { name: 'launch-http-absent', credential: 'absent', expected: null },
  { name: 'launch-loopback-exclusion', launchExcluded: true, expected: 'direct' },
  { name: 'context-overrides-launch-exclusion', launchExcluded: true, contextProxy: true, expected: 'C' },
  {
    name: 'context-loopback-exclusion-no-launch-fallback',
    contextProxy: true,
    contextExcluded: true,
    expected: 'direct',
  },
];
export async function main() {
  assert.ok(CHROME, 'Set CHROME to the browser executable');
  assert.ok(
    !process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK,
    'Use Playwright default loopback handling'
  );
  const snapshot = await resources(),
    execution = capacity(snapshot, requestedPool(), cases.length);
  if (execution.effective < 2) {
    const output = {
      passed: false,
      status: 'resource-preflight-stopped',
      playwrightVersion,
      resources: snapshot,
      execution,
      cases: [],
      externalRequests: 0,
    };
    console.log(JSON.stringify(output, null, 2));
    process.exitCode = 1;
    return output;
  }
  let active = 0,
    maximumActive = 0;
  const results = await pool(
    cases.map(spec => async () => {
      maximumActive = Math.max(maximumActive, ++active);
      try {
        return { ...(await httpCase(spec)), finishedAt: new Date().toISOString() };
      } finally {
        active--;
      }
    }),
    execution.effective
  );
  assert.ok(maximumActive >= 2, 'The local matrix must run concurrently');
  const output = {
    passed: results.every(result => result.status === 'passed'),
    playwrightVersion,
    expectedVersion,
    display: process.env.DISPLAY || null,
    sandbox: 'Playwright default chromiumSandbox:false; isolated owned-origin test',
    forcedLoopbackHandlingDisabled: false,
    resources: snapshot,
    execution,
    maximumActive,
    cases: results,
    scope:
      'Loopback HTTP authentication and observed launch/context routing in this pinned runtime; no external request',
  };
  console.log(JSON.stringify(output, null, 2));
  if (!output.passed) process.exitCode = 1;
  return output;
}
if (process.argv[1] && import.meta.url === pathToFileURL(await fs.realpath(process.argv[1])).href) {
  main().catch(error => {
    console.error(JSON.stringify({ passed: false, error: safeError(error) }));
    process.exitCode = 1;
  });
}

Compara dos salidas reales desde un perfil cerrado

Un par de puertos de proxy no constituye un experimento con dos salidas. Distintos adaptadores locales pueden alcanzar la misma dirección pública ascendente. Antes de comparar valores del navegador, exige que cada ruta autorizada devuelva una dirección pública válida desde un observador propio y que ambas direcciones sean distintas. Si alguna respuesta es una página de autenticación, una traza malformada, una redirección inesperada o un error de red, la comparación está incompleta. No sustituyas esa evidencia por un número de puerto local o una etiqueta de país del proveedor.

Utiliza un perfil ordinario que se haya creado, cerrado correctamente y mantenido después inmutable. Crea dos copias independientes y escribibles a partir de ese origen y compara sus resúmenes del árbol antes de iniciar cualquiera de los navegadores. Las copias deben incluir los mismos archivos seleccionados y aplicar la misma política de exclusión de bloqueos transitorios. Tras la comparación, vuelve a calcular el hash del origen. Que coincida solo el nombre del directorio del perfil no demuestra nada sobre su contenido; compartir un directorio escribible activo entre dos navegadores también introduce bloqueos y condiciones de carrera en el estado.

Mantén constantes el ejecutable del navegador, el sistema operativo, el modo de lanzamiento, el viewport y cualquier configuración regional explícita. Cambia únicamente la ruta autorizada entre los contextos persistentes A y B, creados con launchPersistentContext(). Inicia ambas ejecuciones a la vez, navega una vez con cada una al observador propio y lee los valores del navegador en esa misma página. Esto establece el alcance de la observación: esas dos peticiones, esas dos entradas clonadas y ese entorno. No determina todas las rutas que el navegador puede utilizar durante su vida.

Cloudflare documenta el endpoint /cdn-cgi/trace de cada dominio como recurso de diagnóstico. El observador utilizado aquí está en botbrowser.io, con límites explícitos de peticiones; no hace falta ningún detector, buscador o sitio de destino ajeno. La traza completa puede contener una dirección sin anonimizar, así que compárala y valídala en memoria. Conserva en la evidencia un resumen con clave efímera y específico de la ejecución, en lugar de publicar direcciones o una consulta reutilizable de direcciones. Una etiqueta de ruta describe la configuración autorizada, no garantiza una ubicación física.

Los servicios SOCKS ascendentes autenticados requieren una denominación precisa. Un adaptador HTTP CONNECT local restringido puede autenticarse ante el servicio ascendente mientras el navegador utiliza el protocolo de proxy HTTP ordinario con ese adaptador. Una petición satisfactoria a través de él demuestra que la ruta compuesta funcionó para el destino permitido. No elimina la limitación de contraseñas SOCKS5 nativas de Chromium, y nunca debe describirse como si Playwright aceptara un par de usuario y contraseña SOCKS5. Registra los contadores de túneles del adaptador por separado de los valores de página y las respuestas del observador.

Comparación medida de dos salidas

El mismo entorno Ubuntu fijado ejecutó simultáneamente ambas ejecuciones externas del navegador. Sus navegaciones al observador comenzaron en 2026-11-12T00:01:38.546Z, todavía el 11 de noviembre en Toronto. Cada una hizo una petición aceptada al endpoint de traza propio, devolvió HTTP 200 y superó la validación de dirección pública. Las dos direcciones aceptadas fueron distintas. Los adaptadores restringidos al destino permitieron un túnel ascendente cada uno; los intentos adicionales de conexión se rechazaron localmente. No hubo reintentos externos. Antes de esas peticiones, cuatro comprobaciones locales del cliente del adaptador cubrieron autenticación correcta, credenciales rechazadas, negociación malformada y un destino no permitido; todas pasaron, con cero reenvíos en los tres casos negativos.

Magnitud registradaRuta ARuta B
Estado HTTP del observador / peticiones aceptadas200 / 1200 / 1
HMAC de la dirección pública, primeros 12 caracteresbf448a3168c12ef6c2495efc
Entradas del perfil cerrado / prefijo del resumen inicial134 / cb216f5343b0134 / cb216f5343b0
Zona horariaAmerica/TorontoAmerica/Toronto
Configuración regional Intl resueltaen-USen-US
navigator.languagesen-US, enen-US, en
SHA-256 del canvas, primeros 12 caracteresa37a4010499aa37a4010499a
Comparación del agente de usuarioIgualesIguales
Candidatos ICE host con iceServers: []1; recopilación completa1; recopilación completa

El agente de usuario completo de ambas ejecuciones fue Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/154.0.0.0 Safari/537.36. La comparación de canvas dibujó un canvas de 160 × 50, fondo #e5ebf3, color de texto #17243a, fuente 16px Arial y Proxy fixture 2026 en (5, 30), y después aplicó SHA-256 a canvas.toDataURL(). Esas entradas exactas definen el resumen; no es una puntuación general de huella digital.

Ambas copias de 134 entradas coincidieron con la entrada cerrada antes del lanzamiento, y el resumen de la entrada original permaneció igual después. La igualdad tras el cambio de salida validado demuestra que esos valores de página registrados no siguieron automáticamente las rutas en esta ejecución. No demuestra que se inspeccionara todo el estado del navegador. Las cookies, los permisos, los service workers, el almacenamiento y la actividad de red posterior pueden diferir sin aparecer en esas lecturas. Los recuentos ICE limitados a host no miden la igualdad de direcciones de candidatos ni el tráfico STUN/TURN. Ni las señales iguales ni las salidas distintas determinan la decisión del destino de vincular cuentas.

Interpreta cada valor del navegador según su alcance

La zona horaria procede del entorno del navegador expuesto por Intl.DateTimeFormat().resolvedOptions(), no directamente de la dirección de la respuesta de traza. Una ruta etiquetada para un país puede coexistir con otra zona horaria porque la entrada del navegador y la ruta ascendente son independientes. Un requisito legítimo de prueba puede ser mantener una zona horaria fija en todas las rutas o exigir un entorno regional declarado. Define primero ese requisito; coincidir con una base de datos de IP no es una condición universal de corrección.

navigator.languages, la configuración regional resuelta de Intl y una cabecera de idioma HTTP son observaciones relacionadas, pero distintas. Registra lo que devuelve realmente la página y, si las cabeceras importan, regístralas por separado en un servidor propio. No deduzcas las tres de una sola opción regional. Un ajuste de contexto es una entrada experimental: cambiarlo junto con el proxy impide atribuir una diferencia exclusivamente a la ruta. Esa entrada puede ser útil en una prueba posterior, después de completar la comparación de una sola variable.

La salida del canvas corresponde a un dibujo, una compilación del navegador y un entorno de renderizado concretos. Calcular el hash de un dibujo fijo facilita comparar esa salida; no define un identificador universal del navegador. Conserva las instrucciones de dibujo y el método de comparación. No sorprende que dos rutas produzcan el mismo resumen cuando se mantienen fijas las entradas del renderizador y del perfil subyacentes. Resúmenes distintos requieren investigar las entradas de renderizado antes de atribuirles una interpretación geográfica o relacionada con una cuenta.

WebRTC exige una afirmación especialmente acotada. Una conexión entre pares con iceServers: [] observa la recopilación de candidatos host locales bajo esa configuración. Es posible conservar tipos y recuentos de candidatos sin exponer direcciones. Esto no ejercita STUN o TURN, no demuestra que UDP siga un proxy HTTP ni establece la ausencia de una fuga. No recopilar un candidato tampoco es un veredicto general sobre la red. Comprueba cualquier requisito más amplio mediante un endpoint autorizado por separado y un plan de observación específico del protocolo.

El agente de usuario es otra entrada que registrar junto con el binario exacto. Una UA coincidente no demuestra que ClientHello, HTTP/2 o todas las API de página también coincidan. A la inversa, una diferencia de UA no debe atribuirse a la rotación de proxies sin comprobar la versión del navegador y los ajustes de lanzamiento. Mantén las comparaciones al nivel que sustenta la medición. Un destino puede combinar señales que la página local nunca midió; ningún resultado de aquí certifica su decisión de vinculación de cuentas o de riesgo.

La referencia de alineación de proxy y geolocalización de BotBrowser documenta las entradas de entorno compatibles. Puede ayudar a reproducir una configuración de prueba declarada, pero no elige la política regional correcta para una aplicación ni valida credenciales ascendentes. Corrige un fallo de autenticación HTTP en la capa del proxy; corrige una ruta incorrecta de contexto donde se configura ese contexto; cambia valores del navegador solo cuando un requisito explícito de prueba los exija. Así se conserva el valor causal de la siguiente comparación.

Fuentes

Conclusión práctica

Empieza con un marcador de origen propio y un contador de ruta. Una vez comprendido ese trayecto local, valida dos salidas públicas autorizadas desde entradas de perfil cerrado idénticas byte a byte y compara los valores concretos del navegador que mencione el requisito. Un proxy de Playwright operativo no altera automáticamente esos valores. El siguiente cambio útil es el mínimo en la capa que falló, seguido de la misma comprobación controlada, en lugar de un nuevo conjunto de ajustes de proxy, perfil y renderizado.

Preguntas frecuentes

¿Puede un contexto heredar estado del navegador solo por usar el proxy de lanzamiento?

La herencia de ruta y la persistencia de estado son propiedades independientes. Un newContext() normal crea un contexto aislado y no persistente aunque utilice el valor predeterminado del lanzamiento. Un lanzamiento persistente vincula un contexto a un directorio en disco. Para un experimento con la misma entrada, utiliza una instantánea cerrada y copias escribibles separadas; compartir la dirección del proxy no demuestra que se compartieran cookies o almacenamiento.

¿Una exclusión directa envía una petición fallida al proxy de lanzamiento como alternativa?

Una exclusión selecciona la ruta directa para un destino coincidente, en lugar de una cadena de alternativas. Confírmalo con contadores independientes del proxy de lanzamiento, del proxy de contexto y del origen. Si la propia ruta directa no está disponible, ese fallo no demuestra que se consultara ninguno de los proxies. Mantén constantes la escritura del destino y el esquema al comprobar la regla.

¿Ocultar las direcciones en el informe hace que la medición sea menos útil?

La validez de las direcciones públicas y la comprobación de desigualdad A/B pueden afirmarse antes de descartar los valores originales. Un resumen temporal con clave por ejecución permite al informe mostrar que las respuestas aceptadas difirieron sin almacenar una dirección. La clave secreta no se publica ni reutiliza, y el registro no puede determinar por sí solo el país físico ni las rutas posteriores de ninguna de las salidas del proveedor.

#Playwright#Autenticación De Proxy#SOCKS5#Identidad Del Navegador

Este artículo forma parte de la guía: Primeros pasos con automatización de navegador: mapa inicial

Ejecuta la misma comprobación en BotBrowser

Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.