Solución de problemas11 min de lectura

Corrige el error de proxy 407 sin adivinar

Diagnostica Proxy Authentication Required en un orden repetible: gateway, método de autenticación, codificación de credenciales, alcance de cuenta y ajustes del cliente.

En esta página

Un 407 apunta a la capa de autenticación del proxy. Empieza por ahí antes de cambiar la petición al destino o añadir reintentos. Esta guía da el orden de comprobación, el síntoma exacto en curl, en Python con requests, httpx, aiohttp y urllib3, en Playwright y en Puppeteer, y un ejemplo funcional por cliente. Cada cadena de error y cada ejemplo de abajo se registró contra un pequeño proxy de loopback que exige autenticación Basic; el laboratorio y la ejecución registrada están enlazados al final.

Identifica quién pide las credenciales

HTTP 407 significa que el proxy requiere autenticación. La respuesta incluye un desafío Proxy-Authenticate, y el cliente proporciona las credenciales del proxy mediante Proxy-Authorization, tal como define el RFC 9110. La autenticación del destino es independiente: el inicio de sesión del propio sitio web usa 401 y Authorization. Poner la contraseña del proxy en la cabecera Authorization del destino, o en la opción de autenticación de sitio web de tu cliente, es la solución equivocada; cada sección de cliente más abajo nombra esa opción para que puedas descartarla.

Dónde aparece el 407 depende del destino. Para un destino http:// el proxy reenvía la petición en sí, así que el 407 llega como una respuesta normal: código de estado 407, una cabecera Proxy-Authenticate y a veces un cuerpo breve. Para un destino https:// el cliente envía primero CONNECT host:443 para abrir un túnel. Un 407 en ese paso ocurre antes de cualquier intercambio con el destino, así que la mayoría de los clientes lanzan una excepción de proxy o de túnel en lugar de entregarte un objeto de respuesta. curl 8.15, 8.16 y 8.17, por ejemplo, imprimen curl: (56) CONNECT tunnel failed, response 407; curl 8.18.0 y 8.22.0 imprimieron el mismo mensaje con código de salida 7 en nuestras ejecuciones. Mismo proxy, mismas credenciales, síntomas distintos:

ClienteDestino https:// (CONNECT)Destino http://
requests 2.34requests.exceptions.ProxyError con Tunnel connection failed: 407 Proxy Authentication RequiredResponse con status_code == 407
httpx 0.28httpx.ProxyError: 407 Proxy Authentication RequiredResponse con status_code == 407
aiohttp 3.14aiohttp.ClientHttpProxyError: 407, message='Proxy Authentication Required'respuesta con status == 407
urllib3 2.8urllib3.exceptions.ProxyError con Tunnel connection failed: 407respuesta con status == 407
Playwright 1.63con el waitUntil: "load" por defecto, page.goto rechaza con Timeout 30000ms exceeded y requestfailed informa net::ERR_TUNNEL_CONNECTION_FAILED; con waitUntil: "commit" se resuelve con status() === 407page.goto se resuelve con estado 407 y el cuerpo de error del proxy
Puppeteer 25page.goto rechaza con net::ERR_INVALID_AUTH_CREDENTIALS (sin credenciales) o se resuelve con estado 407 más net::ERR_HTTP_RESPONSE_CODE_FAILURE (credenciales incorrectas)rechaza con net::ERR_INVALID_AUTH_CREDENTIALS
curl 8.15 a 8.17salida 56, curl: (56) CONNECT tunnel failed, response 407, %{http_connect} es 407http=407
curl 8.18.0salida 7, curl: (7) CONNECT tunnel failed, response 407, %{http_connect} es 407http=407

Si el código que ves es 429, 502 o 504 en lugar de 407, los errores de proxy explicados identifica qué capa lo produjo. Cuando Chromium informa del fallo de CONNECT sin estado, la guía de errores de túnel cubre el resto de su familia net::ERR_*. Scrapy no está en la tabla: Scrapy 2.19 lanza TunnelError: Could not open CONNECT tunnel with proxy para una URL https:// y descarta el 407 de una URL http:// con una sola línea INFO en el registro; Proxies en Scrapy 2.19 cubre ambos casos. curl_cffi tampoco está en la tabla: la 0.16.3 lanza ConnectionError con curl: (7) CONNECT tunnel failed, response 407 para una URL https://, no ProxyError, y devuelve el 407 como una respuesta normal para una URL http://; Proxies en curl_cffi muestra cómo capturar ambos.

Comprueba una variable cada vez

Sigue este orden para obtener una reproducción útil en lugar de una secuencia de cambios inconexos. La configuración heredada es la sorpresa habitual; la guía de variables de entorno de proxy explica cómo ver qué gateway eligió realmente un cliente.

  • Gateway: copia el esquema, el nombre de host y el puerto exactos de las instrucciones de configuración actuales de la cuenta.
  • Método: confirma si este endpoint espera usuario/contraseña, una IP de origen autorizada u otro mecanismo de autenticación documentado. Una contraseña enviada a un puerto que autentica por IP de origen se responde con 407 desde una máquina nueva.
  • Alcance de las credenciales: confirma que el nombre de usuario pertenece a este producto o subusuario, y no solo al inicio de sesión del panel.
  • Codificación: codifica las credenciales con percent-encoding cuando las incrustes en una URL; usa campos de credenciales separados cuando el cliente lo permita.
  • Cliente: revisa la configuración de proxy heredada y verifica que el gateway previsto es realmente el seleccionado. Una HTTPS_PROXY obsoleta, un .curlrc, el entorno de un contenedor o un secreto de CI pueden apuntar a otro gateway o a una contraseña antigua. Registra el host y el puerto que resolvió el cliente, nunca la contraseña.

Empieza con una sola petición curl

Antes de tocar el código de la aplicación, demuestra que el gateway, el usuario y la contraseña funcionan siquiera. Las credenciales del proxy son distintas de las del destino: -U o --proxy-user las envía al proxy; -u o --user las envía al destino. Con la opción equivocada el proxy sigue sin ver Proxy-Authorization y responde 407, aunque la contraseña sea correcta.

sh
curl --disable -sS -o /dev/null --noproxy '' \
  -x "$PROXY_URL" --proxy-user "$PROXY_USERNAME:$PROXY_PASSWORD" \
  -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' \
  https://example.com/

connect=200 http=200 significa que las credenciales se aceptan y que el problema está en tu aplicación. connect=407 con CONNECT tunnel failed, response 407 significa que la propia cuenta no está siendo aceptada; revisa el gateway, el método y el alcance de arriba y después envía el informe descrito al final. La guía de proxy para curl tiene la referencia completa de opciones.

Compara curl con la aplicación que falla

Si la petición con curl funciona, concéntrate en cómo la aplicación traslada esa configuración. En Playwright, por ejemplo, las credenciales del proxy van en proxy.username y proxy.password, no en httpCredentials. Las secciones de cliente de abajo muestran la excepción o el estado exacto que produce cada cliente y un ejemplo funcional para cada uno.

Si ambos clientes fallan, elimina los modificadores opcionales de país o sesión específicos del proveedor y prueba el formato de credenciales documentado más simple; después añade un modificador cada vez. Confirma con el proveedor el acceso a la cuenta y las instrucciones de autenticación del endpoint. No asumas que todos los proveedores usan 407 para la misma condición de facturación o de acceso al producto.

Python requests: ProxyError, Tunnel connection failed: 407

Requests toma las credenciales del proxy únicamente de la URL del proxy. No existe un argumento aparte de autenticación de proxy que sobreviva al paso CONNECT: requests.auth.HTTPProxyAuth añade una cabecera Proxy-Authorization a la petición, lo que funciona para un destino http:// y no hace nada para uno https://, porque urllib3 abre el túnel antes de que esa cabecera llegue a enviarse. Lo mismo ocurre con una entrada Proxy-Authorization en headers=. En la ejecución registrada ambas variantes devolvieron 200 para http://example.com/ y ProxyError para https://example.com/.

Tres cosas más rompen la forma de URL:

  • Caracteres especiales. Una contraseña como p@ss/w0rd#1 pegada tal cual en http://user:p@ss/w0rd#1@host:port hace que el analizador de URL trate ss como el host. Requests falla entonces con NameResolutionError para el host 'ss', lo que no se parece en nada a un problema de autenticación. Codifica con porcentaje el usuario y la contraseña por separado con urllib.parse.quote(value, safe="").
  • Ambas claves. El diccionario proxies se indexa por el esquema del destino. Un diccionario con solo una entrada "http" envía las peticiones https:// directamente, sin ningún proxy. Asigna a "http" y a "https" la misma URL autenticada.
  • Las variables de entorno ganan a la Session. Si HTTPS_PROXY está definida en el entorno, Session.proxies pierde frente a ella, y una variable sin credenciales convierte una configuración correcta de Session en un 407. Un argumento proxies= en la propia petición sigue ganando. La solución más limpia es session.trust_env = False, que también desactiva la herencia de NO_PROXY; la guía de variables de entorno explica qué más controla ese ajuste.

El ejemplo de abajo aplica las tres. Es el archivo fix_requests.py de la descarga del laboratorio y se ejecutó con una contraseña normal, una contraseña con caracteres especiales y una contraseña incorrecta; los dos primeros casos imprimieron 200, el tercero imprimió el mensaje de ProxyError.

python
import os
from urllib.parse import quote

import requests

# PROXY_URL is scheme://host:port with no credentials inside, for example http://gate.example.net:8080
proxy_url = os.environ["PROXY_URL"]
username = quote(os.environ["PROXY_USERNAME"], safe="")
password = quote(os.environ["PROXY_PASSWORD"], safe="")
target = os.environ.get("TARGET_URL", "https://example.com/")

scheme, host_port = proxy_url.split("://", 1)
authenticated_proxy = f"{scheme}://{username}:{password}@{host_port}"
proxies = {"http": authenticated_proxy, "https": authenticated_proxy}

session = requests.Session()
session.trust_env = False  # HTTP_PROXY, HTTPS_PROXY and NO_PROXY can no longer override these proxies

try:
    response = session.get(target, proxies=proxies, timeout=(5, 15))
    print(response.status_code)
except requests.exceptions.ProxyError as error:
    print("proxy rejected the request:", error)

La guía de proxy para Python Requests cubre Sessions, SOCKS y rotación por encima de esto; el ejemplo de aquí es el mínimo que hace desaparecer un 407.

Python httpx: httpx.ProxyError: 407 Proxy Authentication Required

httpx tiene un objeto dedicado para esto: httpx.Proxy(url, auth=(username, password)). Las credenciales van en la petición CONNECT y nunca tocan la URL, así que una contraseña con caracteres especiales no necesita codificación. El argumento auth= de httpx.get o del Client es autenticación de sitio web; en la ejecución registrada dejó el desafío del proxy sin responder y lanzó httpx.ProxyError: 407 Proxy Authentication Required. Las credenciales dentro de la URL del proxy también funcionan cuando están codificadas con porcentaje; pegada en bruto, la misma contraseña p@ss/w0rd#1 produjo httpx.ConnectError: [Errno -3] Temporary failure in name resolution, de nuevo porque el analizador leyó la contraseña como un nombre de host.

python
import os

import httpx

proxy = httpx.Proxy(
    url=os.environ["PROXY_URL"],  # scheme://host:port, no credentials inside
    auth=(os.environ["PROXY_USERNAME"], os.environ["PROXY_PASSWORD"]),
)
target = os.environ.get("TARGET_URL", "https://example.com/")

with httpx.Client(proxy=proxy, trust_env=False, timeout=15.0) as client:
    try:
        response = client.get(target)
        print(response.status_code)
    except httpx.ProxyError as error:
        print("proxy rejected the request:", error)

httpx.AsyncClient acepta el mismo argumento proxy=, y también httpx.HTTPTransport si solo enrutas algunos patrones de URL por el proxy con mounts. Ambos se probaron en el laboratorio y devolvieron 200. Para un worker totalmente asíncrono, la guía de proxy asíncrono con httpx se construye sobre el mismo objeto Proxy.

Python aiohttp: ClientHttpProxyError 407

aiohttp separa las dos credenciales de forma explícita: proxy_auth=aiohttp.BasicAuth(username, password) es para el proxy, auth= es para el sitio web. Pasar el login del proxy mediante auth= lanzó aiohttp.client_exceptions.ClientHttpProxyError: 407, message='Proxy Authentication Required' en la ejecución registrada. Las credenciales incrustadas en la URL del proxy también se aceptan. aiohttp 3.14 declara obsoletos proxy_auth y BasicAuth: la llamada de abajo sigue funcionando, pero imprime dos DeprecationWarning. Proxies en aiohttp 3.14 muestra el sustituto y el caso en que no autentica.

Una trampa específica de aiohttp: ClientSession ignora HTTP_PROXY y HTTPS_PROXY a menos que la crees con trust_env=True. Con el valor por defecto, una sesión sin argumento proxy= se conecta directamente, así que un script que "funcionaba en la otra máquina porque allí la variable estaba definida" deja de usar el proxy en silencio en lugar de fallar con 407.

python
import asyncio
import os

import aiohttp


async def main() -> None:
    proxy_url = os.environ["PROXY_URL"]  # scheme://host:port, no credentials inside
    proxy_auth = aiohttp.BasicAuth(os.environ["PROXY_USERNAME"], os.environ["PROXY_PASSWORD"])
    target = os.environ.get("TARGET_URL", "https://example.com/")
    timeout = aiohttp.ClientTimeout(total=15)

    async with aiohttp.ClientSession(timeout=timeout) as session:
        try:
            async with session.get(target, proxy=proxy_url, proxy_auth=proxy_auth) as response:
                print(response.status)
        except aiohttp.ClientHttpProxyError as error:
            print("proxy rejected the request:", error.status, error.message)


asyncio.run(main())

urllib3 y urllib.request

Si usas urllib3 directamente, ProxyManager no lee las credenciales de la URL del proxy para el túnel: ProxyManager("http://user:pass@host:port") siguió produciendo ProxyError con Tunnel connection failed: 407 para un destino https://. Pásalas en su lugar como proxy_headers=urllib3.util.make_headers(proxy_basic_auth="user:pass"). El argumento headers= de request() tampoco se usa para el CONNECT. El urllib.request.ProxyHandler de la biblioteca estándar es el caso contrario: sí analiza http://user:pass@host:port y respondió correctamente al desafío en la misma ejecución.

Playwright: Timeout exceeded, estado 407 y net::ERR_TUNNEL_CONNECTION_FAILED

Playwright configura el proxy mediante la opción proxy, ya sea en browserType.launch o en browser.newContext, con tres campos: server, username y password. Cuando las credenciales faltan o son incorrectas, el síntoma depende de a qué espera page.goto. Con el waitUntil: "load" por defecto, y con "domcontentloaded", nunca se carga ningún documento, así que la llamada rechaza con page.goto: Timeout 30000ms exceeded tras agotar el tiempo de navegación completo, y la única pista es un evento requestfailed que lleva net::ERR_TUNNEL_CONNECTION_FAILED. Por eso un 407 del proxy en Playwright se reporta tan a menudo como "el proxy es lento". Con waitUntil: "commit" la misma navegación se resuelve en milisegundos con una respuesta cuyo status() es 407, cuyo statusText() es Proxy Authentication Required y cuyas cabeceras incluyen el Proxy-Authenticate del proxy. El ejemplo de abajo espera primero a commit, comprueba el estado y solo entonces espera a domcontentloaded; en la ejecución registrada el caso de contraseña incorrecta falló en menos de un segundo con el mensaje 407 en lugar de a los treinta segundos.

Dos ubicaciones parecen correctas y no lo son:

  • Credenciales dentro de server. { server: "http://user:pass@host:port" } se acepta sin queja, y el proxy recibe entonces un CONNECT sin ningún Proxy-Authorization. Chromium ignora la parte userinfo de una URL de proxy.
  • httpCredentials en el contexto. En la ejecución registrada, Chromium 153 sí respondió al desafío del proxy con httpCredentials, así que la navegación tuvo éxito. Sigue siendo el lugar equivocado: httpCredentials es el login del sitio web, y Playwright ofrecerá el mismo usuario y contraseña a cualquier origen que responda 401. Mantén la contraseña del proxy en proxy.username y proxy.password.
javascript
import { chromium } from 'playwright';

const proxy = {
  server: process.env.PROXY_URL, // scheme://host:port, no credentials inside
  username: process.env.PROXY_USERNAME, // proxy credentials belong here, not in httpCredentials
  password: process.env.PROXY_PASSWORD,
};
const target = process.env.TARGET_URL ?? 'https://example.com/';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({ proxy });
  const page = await context.newPage();
  page.on('requestfailed', (request) => console.error('failed:', request.url(), request.failure()?.errorText));
  // Wait for 'commit' first: a proxy 407 never reaches 'domcontentloaded', it would surface as a TimeoutError instead.
  const response = await page.goto(target, { timeout: 30_000, waitUntil: 'commit' });
  if (!response || response.status() === 407) {
    throw new Error(`proxy rejected the credentials: ${response?.status()} ${response?.statusText()}`);
  }
  await page.waitForLoadState('domcontentloaded');
  console.log(response.status());
} finally {
  await browser.close();
}

Un proxy por contexto necesita primero chromium.launch({ proxy: { server: 'per-context' } }); la ejecución del laboratorio confirma que un contexto configurado así se autentica por sí solo. La API request.newContext usada para pruebas de API acepta el mismo objeto proxy y devolvió 200 con credenciales y 407 sin ellas. Los caracteres especiales en la contraseña no necesitan codificación aquí porque Playwright nunca construye una URL a partir de ellos. Hay un límite que no es de Playwright: Chromium no admite usuario y contraseña en proxies SOCKS5, como indica su documentación de proxy, así que una pasarela socks5:// que exija login no puede usarse desde Chromium en absoluto, y la guía HTTP vs SOCKS5 ayuda a elegir la pasarela HTTP en su lugar. La guía de proxy para Playwright muestra cómo mantener esta configuración aislada por escenario.

Puppeteer: net::ERR_INVALID_AUTH_CREDENTIALS

Puppeteer pasa el proxy a Chromium como un flag de lanzamiento, --proxy-server=host:port, y no hay flag para la contraseña. Sin una llamada a page.authenticate, la primera navegación rechaza con net::ERR_INVALID_AUTH_CREDENTIALS; es Chromium informando de que llegó un desafío del proxy y no había nada registrado para responderlo. Con una contraseña incorrecta, page.goto se resuelve con estado 407 y la propia página de error de Chromium en el documento, y requestfailed informa net::ERR_HTTP_RESPONSE_CODE_FAILURE.

Lo que no funciona, según la ejecución registrada:

  • --proxy-server=http://user:pass@host:port falla de inmediato con net::ERR_NO_SUPPORTED_PROXIES.
  • page.setExtraHTTPHeaders({ 'Proxy-Authorization': ... }) falla con net::ERR_INVALID_ARGUMENT; Chromium se niega a que el código de la página establezca esa cabecera.

Lo que sí funciona: llama a page.authenticate({ username, password }) en la página antes de su primera navegación. Las credenciales quedan entonces en caché para el contexto del navegador, así que una segunda página abierta con browser.newPage() cargó sin su propia llamada, mientras que una página en un contexto nuevo de browser.createBrowserContext() falló con ERR_INVALID_AUTH_CREDENTIALS hasta que tuvo la suya. Llamar a page.authenticate después de una navegación fallida y reintentar page.goto también funcionó, lo que resulta útil cuando las credenciales solo se conocen más tarde.

javascript
import puppeteer from 'puppeteer';

const target = process.env.TARGET_URL ?? 'https://example.com/';
const browser = await puppeteer.launch({
  args: [`--proxy-server=${process.env.PROXY_URL}`], // scheme://host:port, credentials are not accepted here
});
try {
  const page = await browser.newPage();
  // Register the proxy credentials before the first navigation; every new browser context needs its own call.
  await page.authenticate({ username: process.env.PROXY_USERNAME, password: process.env.PROXY_PASSWORD });
  const response = await page.goto(target, { timeout: 30_000, waitUntil: 'domcontentloaded' });
  if (!response || response.status() === 407) {
    throw new Error(`proxy rejected the credentials: ${response?.status()}`);
  }
  console.log(response.status());
} finally {
  await browser.close();
}

El código Node.js que solo necesita HTTP, no un navegador, no debería lanzar Chromium para esto; la guía de proxy con fetch en Node.js pone las mismas credenciales en un ProxyAgent de Undici.

Envía un informe reproducible y sin datos sensibles

Conserva la versión del cliente, la marca de tiempo en UTC, el host y el puerto del gateway, el nombre de host del destino, el método de autenticación y el estado o la clase de excepción. Indica si la reproducción mínima con curl también falla. Esos detalles permiten al soporte localizar el intento sin recibir tu contraseña.

Elimina de los registros los tokens, las URL con credenciales, las cookies y las cabeceras de autorización. Detén los reintentos automáticos mientras diagnosticas un 407 persistente: un bucle de reintentos convierte un 407 en una ráfaga de logins fallidos, y una respuesta perdida, dos trabajos creados explica por qué los reintentos a ciegas también hacen daño por otras razones. Una vez corregido, vuelve a ejecutar una petición y después el escenario de aplicación más pequeño antes de restaurar la concurrencia normal.

Para obtener ayuda dentro de un agente de programación compatible con MCP, Proxy Toolkit MCP ofrece diagnóstico de errores y plantillas de configuración compatibles. Describe el cliente y el error sin datos sensibles, sin enviar credenciales del proxy ni registros completos; verifica cualquier cambio sugerido con la reproducción acotada de arriba.

  • Identifica la autenticación del proxy por separado de la autenticación del sitio web.
  • Reproduce con la misma configuración en un segundo cliente.
  • Comparte diagnósticos sin datos sensibles en lugar de credenciales o trazas en bruto.

Reproduce la ejecución registrada

Los síntomas de los clientes y los ejemplos de arriba se registraron con el laboratorio de la carpeta de descarga: un proxy falso en 127.0.0.1 que exige autenticación Basic y reenvía a https://example.com/, el script symptoms.py que produce cada cadena de error de Python, los casos de Playwright, Puppeteer y curl, y los cinco ejemplos fix_* ejecutados con una contraseña normal, una contraseña con caracteres especiales y una contraseña incorrecta. El results.json registrado nombra las versiones usadas: Python 3.14, requests 2.34, httpx 0.28, aiohttp 3.14, urllib3 2.8, Node.js 24, Playwright 1.63 con Chromium 153, Puppeteer 25 y curl 8.18. Los mensajes de curl 8.15 a 8.17 y 8.22.0 proceden del laboratorio de loopback publicado con CONNECT tunnel failed, response 403. Las versiones más recientes pueden cambiar una cadena de error; el mecanismo, credenciales del proxy en la opción de proxy y nada más, no cambia.

Fuentes y lecturas adicionales

Referencias técnicas usadas para esta guía. Consulta la documentación de tu versión instalada y la configuración compatible de tu proveedor.