Tunnel connection failed: 502/503 con proxy: causas y soluciones

Un 502, 503 o 504 durante CONNECT viene del proxy; un 502 después de abrir el túnel viene del sitio. Cómo distinguirlos en cada cliente y qué corregir.

En esta página

Un 502 Bad Gateway, 503 Service Unavailable o 504 Gateway Timeout que llega mientras el cliente abre el túnel del proxy lo envía el proxy: el túnel nunca se abrió y el sitio web nunca vio tu petición. Un 502 que llega después de abrirse el túnel lo envía el sitio web, o algo que está delante de él. Mismo número, distinto responsable, distinta solución. Cada cadena de error y cada número de abajo provienen de un laboratorio en loopback ejecutado el 5 de noviembre de 2026, enlazado al final.

Quién envió el 502

Para una URL https://, el cliente primero pide al proxy un túnel con CONNECT host:443 (RFC 9110). Si el proxy responde algo distinto de 2xx, no hay túnel y la mayoría de los clientes lanzan una excepción. Si el túnel se abre y el sitio responde 502, recibes una respuesta normal.

ClienteEl proxy rechazó el túnelEl sitio respondió 502
curl 8.5.0código de salida 56, curl: (56) CONNECT tunnel failed, response 502código de salida 0, %{http_code} es 502, %{http_connect} es 200
curl 8.18.0código de salida 7, curl: (7) CONNECT tunnel failed, response 502código de salida 0, %{http_code} es 502, %{http_connect} es 200
Requests 2.34requests.exceptions.ProxyError con Tunnel connection failed: 502 Bad GatewayResponse con status_code == 502
HTTPX 0.28httpx.ProxyError: 502 Bad GatewayResponse con status_code == 502
aiohttp 3.14ClientHttpProxyError: 502, message='Bad Gateway'respuesta con status == 502
Playwright 1.63page.goto: net::ERR_TUNNEL_CONNECTION_FAILEDpage.goto se resuelve, status() es 502
Node.js 24 fetchTypeError: fetch failed, y err.cause.cause.message es Proxy response (502) !== 200 when HTTP Tunnelingresponse.status es 502

503 y 504 siguen el mismo patrón con su propio número, por ejemplo Tunnel connection failed: 503 Service Unavailable o curl: (7) CONNECT tunnel failed, response 504. Chromium informó net::ERR_TUNNEL_CONNECTION_FAILED en los tres casos, sin el estado; la guía de ERR_TUNNEL_CONNECTION_FAILED cubre esa familia de errores.

Requests dice Max retries exceeded aunque por defecto no reintenta nada: el proxy del laboratorio recibió un solo CONNECT por petición. Node esconde el estado dos niveles más abajo, así que registra toda la cadena de cause.

Hay dos casos que rompen la regla. Para una URL http:// simple no hay túnel, así que curl, Requests, HTTPX, aiohttp y Playwright devuelven el 502 del propio proxy como una respuesta; compara su cabecera Server y su cuerpo con los habituales del sitio. El fetch de Node también abre un túnel para las URL http://, así que ahí sigue siendo una excepción. Y un proxy que cierra la conexión sin estado no da ningún número: curl imprimió curl: (56) Proxy CONNECT aborted, Requests lanzó ConnectionError con RemoteDisconnected('Remote end closed connection without response'), HTTPX RemoteProtocolError: Server disconnected without sending a response. y Chromium net::ERR_EMPTY_RESPONSE.

Comprobación con una sola petición de curl

Imprime los dos códigos de estado de la URL que falla a través del mismo proxy:

sh
curl --disable -sS -o /dev/null -x "$PROXY_URL" \
  -w 'connect=%{http_connect} http=%{http_code} exit=%{exitcode}\n' \
  https://example.com/
  • connect=502 http=000: el proxy rechazó el túnel; mira las causas de abajo.
  • connect=200 http=502: el sitio respondió 502; mira cuando el 502 lo envió el sitio web.
  • connect=000 con Proxy CONNECT aborted: el proxy cerró la conexión sin estado. Revisa primero el puerto.

Causas en el lado del proxy

El sitio no es accesible desde la salida (502). Falló la conexión del proxy hacia el destino; en el laboratorio, una conexión rechazada se convirtió en 502 Bad Gateway en todos los clientes. Pide la URL sin proxy y a través de otra salida o país. Si solo falla una salida, el sitio está caído o bloquea esa red desde allí, así que cambia de salida. Si fallan todas, revisa el host y el puerto de tu línea CONNECT.

La salida está desconectada o rotando (503, a veces 502). La pasarela no tiene adónde enviar la petición. Reintenta las peticiones idempotentes con espera, como se muestra abajo, y revisa el estado de la salida en el panel de tu proveedor.

El proxy dejó de esperar (504). Venció el plazo propio del proxy para llegar al sitio. El proxy del laboratorio esperó 3 segundos y todos los clientes fallaron tras 3,0 a 3,5 segundos. Mantén el timeout de tu cliente por encima del plazo del proxy, o verás tu propio timeout en lugar del 504; la guía de timeouts de proxy separa ambos casos.

Puerto o protocolo equivocado. Apuntamos todos los clientes, como proxy HTTP, a un puerto que solo habla SOCKS5 (pproxy 2.7.9). El servidor cerró la conexión sin respuesta, así que no hubo ningún 502: curl 8.5.0 y 8.18.0 imprimieron curl: (56) Proxy CONNECT aborted, aiohttp lanzó ServerDisconnectedError y el fetch de Node no terminó hasta que saltó su timeout de 10 segundos. Copia el esquema, el host y el puerto de la página de configuración de tu proveedor. Una URL de proxy https:// en un puerto HTTP normal falla de otra forma; consulta SSL WRONG_VERSION_NUMBER detrás de un proxy.

Capacidad del proveedor. Una pasarela sin capacidad puede responder 502 o 503 a los túneles nuevos. Reduce la concurrencia, espera y envía a tu proveedor las horas de los fallos en UTC.

En el mismo paso, un 407 es cuestión de autenticación (corregir el error de proxy 407) y un 403 es un rechazo deliberado (CONNECT tunnel failed, response 403).

Proxies móviles y rotación de IP

En un proxy móvil la salida es un dispositivo en la red de un operador. Mientras cambia de IP puede haber un momento sin salida que funcione; según la pasarela, el cliente ve entonces un 502 o 503 en CONNECT o una conexión cerrada, notificados como se describe arriba. No medimos una rotación real, así que esta guía no da duraciones. Si inicias tú la rotación, pausa las peticiones hasta que funcione una petición de prueba barata.

El laboratorio simuló una salida desconectada durante 4 segundos, con una llamada de Requests por segundo. Los segundos 0 a 3 fallaron con Tunnel connection failed: 503 Service Unavailable; desde el segundo 4 todas las peticiones devolvieron 200. Los 4 segundos son un ajuste del laboratorio: el experimento muestra la forma de una rotación, no su duración.

Los reintentos integrados tratan los dos lados de forma distinta. curl --retry 2 envió el CONNECT al proxy que fallaba una sola vez y se detuvo, pero pidió tres veces al sitio su 502; con --retry-all-errors también repitió el CONNECT. Retry(total=2, status_forcelist=[502, 503, 504]) de urllib3 repitió ambos. El fetch de Node no reintentó nada después de un estado, pero cuando el proxy cerró la conexión sin estado, se reconectó 160.357 veces en 10 segundos, hasta que lo detuvo un AbortSignal.timeout. Pasa siempre una señal.

Reintenta solo peticiones GET. Un CONNECT fallido nunca llegó al sitio, pero un 502 del sitio puede llegar después de que el sitio hiciera el trabajo, y un POST reintentado se ejecuta dos veces; consulta una respuesta perdida, dos trabajos creados. Esta función reintenta un GET tras un 502, 503 o 504 del proxy o del sitio con esperas limitadas y aleatorias, y se detiene ante cualquier otra cosa, como un 407:

python
import random, re, time
import requests

RETRYABLE = {502, 503, 504}
TUNNEL_STATUS = re.compile(r"Tunnel connection failed: (\d{3})")

def get_with_retry(session, url, attempts=5, base_delay=1.0, max_delay=8.0, **kwargs):
    for attempt in range(1, attempts + 1):
        try:
            response = session.get(url, **kwargs)
        except requests.exceptions.ProxyError as exc:
            match = TUNNEL_STATUS.search(str(exc))  # Requests reports the CONNECT status only as text
            if match is None or int(match.group(1)) not in RETRYABLE or attempt == attempts:
                raise
        else:
            if response.status_code not in RETRYABLE or attempt == attempts:
                return response
        time.sleep(min(max_delay, base_delay * 2 ** (attempt - 1)) * random.uniform(0.5, 1.0))

En la ejecución registrada superó la ventana de 4 segundos sin salida en su cuarto intento, tras 5,7 segundos. Frente a un sitio que seguía respondiendo 502 se rindió tras cinco intentos y 9,9 segundos, y devolvió el 502.

Cuando el 502 lo envió el sitio web

Si el túnel se abrió, el proxy hizo su trabajo: el 502 vino de la pasarela, el balanceador de carga o la CDN del sitio, que no obtuvieron una respuesta útil de la aplicación que tienen detrás. Cambiar de proxy o rotar la IP no lo arregla. Pide la URL sin proxy; si también falla, espera y reintenta los GET con la función de arriba. Si solo falla a través del proxy, puede que el sitio esté sirviendo errores a esa red; compara el cuerpo y las cabeceras con una petición directa antes de cambiar de salida.

Reproducir el laboratorio

La carpeta de descarga contiene lab.mjs (un proxy en 127.0.0.1 que responde a CONNECT con 502, 503 o 504 o corta la conexión, y un origen TLS que responde 502 por sí mismo), un script por cliente, wrong_port.mjs, node_checks.mjs y retry_get.py con la función de arriba. results.json registra las versiones: curl 8.5.0 y 8.18.0, Python 3.14.4, Requests 2.34.2, urllib3 2.8.0, HTTPX 0.28.1, aiohttp 3.14.5, Node.js 24.22.1 con undici 7.30.1 y Playwright 1.63.0 con Chromium 153. Sin probar: macOS y Windows, proxies SOCKS y HTTPS, y pasarelas reales de proveedores.

Dentro de un agente de programación, Proxy Toolkit MCP ofrece diagnóstico de errores; dale el cliente y el error depurado, nunca credenciales.

Los proxies móviles de ipvolt son dispositivos dedicados en redes de operadores; rotas la IP cuando quieras o según un horario, así que una rotación nunca te pilla por sorpresa.

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.