Soluciona ProxyError, SSLError y ConnectTimeout en Requests

requests.exceptions.ProxyError es un síntoma, no una causa. Aquí se decodifica la jerarquía de excepciones, cada traceback se asocia con su solución real, y se presenta una función de verificación de salud que clasifica fallos y rota alrededor de salidas inactivas.

requests.exceptions.ProxyError es uno de los mensajes de error menos útiles en Python: se activa por un proxy inactivo, credenciales incorrectas, un esquema erróneo, una salida sobrecargada y un bloqueo de firewall, todos con trazas casi idénticas. El truco para solucionarlo rápidamente es saber que ProxyError no es una causa raíz, es una categoría. En el código fuente de Requests, ProxyError, SSLError y ConnectTimeout son subclases de ConnectionError, y cada uno se activa en una etapa específica del ciclo de vida de la solicitud. Lee la etapa y leerás la causa. Esta guía decodifica la jerarquía de excepciones, asocia cada traceback común con su solución real, y te ofrece una función de verificación de salud que clasifica fallos y rota automáticamente alrededor de salidas moribundas.

La jerarquía de excepciones de requests

Cada error relacionado con proxy en Requests desciende de RequestException. La rama útil para depurar es ConnectionError, porque las tres excepciones que realmente encuentras están bajo ella:

Debido a que los primeros tres comparten un padre, un solo except requests.exceptions.ConnectionError los captura a todos para la lógica de reintento, mientras que capturar las subclases individualmente te permite registrar por qué falló cada uno. La configuración limpia que evita la mayoría de estos se cubre en nuestra guía de proxy de Python Requests; esta publicación trata sobre qué hacer una vez que el traceback ya está en pantalla.

Un hábito ahorra más tiempo que cualquier solución única: lee el traceback de abajo hacia arriba. Requests envuelve el fallo subyacente de urllib3, por lo que los marcos superiores describen dónde se realizó la llamada y los marcos inferiores describen qué salió mal. La línea que quieres es la cláusula Caused by más interna: nombra el fallo concreto (una conexión rechazada, una discrepancia de certificado, un error de puerto analizado) que el ProxyError o ConnectionError externo simplemente está re-lanzando. Una vez que puedas leer esa línea, el resto de esta guía es una tabla de consulta.

El clásico ValueError antes de ProxyError

El traceback de proxy más buscado no es ni siquiera un ProxyError — es ValueError: invalid literal for int() with base 10, lanzado profundamente dentro de urllib3. Ocurre cuando incrustas credenciales en el valor del proxy sin un esquema, por lo que el analizador lee el texto después del colon como un número de puerto:

# Broken — no scheme, so 'pass@host' is parsed as host:port
proxies = {"https": "user:pass@45.11.22.33:8000"}
# -> ValueError: invalid literal for int() with base 10: 'pass@45.11.22.33'

# Fixed — scheme in front, password URL-encoded if it has @ : or /
from urllib.parse import quote
pw = quote("p@ss:word", safe="")
proxies = {
    "http":  f"http://user:{pw}@gate.quantumproxies.io:PORT",
    "https": f"http://user:{pw}@gate.quantumproxies.io:PORT",
}
Diagrama de flujo que muestra dónde se activa cada excepción de requests de Python a lo largo del ciclo de vida de la solicitud: MissingSchema, ProxyError, SSLError y ReadTimeout
La etapa del ciclo de vida nombra la causa: un error tipográfico en tiempo de construcción es MissingSchema, un fallo de conexión al proxy es ProxyError, un fallo de TLS es SSLError, una salida lenta es ReadTimeout.

Una verificación de salud del proxy que clasifica fallos

En lugar de adivinar, captura cada tipo de excepción y conviértelo en un veredicto en inglés sencillo. Esta función devuelve la IP de salida en caso de éxito y una razón etiquetada en caso de fallo — colócala frente a cualquier scrape para confirmar que el proxy está vivo antes de gastar solicitudes en él. Funciona contra cualquier gateway autenticado, incluidos proxies residenciales:

import requests

def check_proxy(proxies, url="https://httpbin.org/ip", timeout=(5, 20)):
    try:
        r = requests.get(url, proxies=proxies, timeout=timeout)
        r.raise_for_status()
        return True, r.json().get("origin")
    except requests.exceptions.ProxyError as e:
        return False, f"proxy unreachable or auth rejected: {e}"
    except requests.exceptions.SSLError as e:
        return False, f"TLS failed (https:// in the https key?): {e}"
    except requests.exceptions.ConnectTimeout:
        return False, "proxy did not answer within the connect window"
    except requests.exceptions.ReadTimeout:
        return False, "target too slow after connect (exit quality)"
    except requests.exceptions.RequestException as e:
        return False, f"other request error: {e}"

ok, detail = check_proxy(proxies)
print("OK" if ok else "FAIL", detail)

Cuando curl funciona pero Python lanza ProxyError

Si las mismas credenciales tienen éxito en curl pero lanzan ProxyError en Python, casi siempre una variable de entorno está sobrescribiendo tu diccionario. Requests lee HTTP_PROXY, HTTPS_PROXY y NO_PROXY desde el shell, y un valor corporativo obsoleto redirige silenciosamente cada llamada. Imprime session.proxies para ver qué se está usando realmente, luego desactiva la búsqueda de entorno por completo:

import requests

session = requests.Session()
session.trust_env = False          # ignore HTTP_PROXY / HTTPS_PROXY from the shell
session.proxies = {
    "http":  "http://USER:PASS@gate.quantumproxies.io:PORT",
    "https": "http://USER:PASS@gate.quantumproxies.io:PORT",
}
print(session.get("https://httpbin.org/ip", timeout=(5, 20)).json())

Un 407 que sobrevive a credenciales correctas apunta a un plan de lista blanca de IP llamado desde una dirección no registrada — la lista completa de causas está en nuestra guía de 407 Proxy Authentication Required.

ProxyError intermitente: rota, no reinicies

El caso frustrante es un código que corre durante veinte minutos, luego lanza ProxyError, y luego vuelve a funcionar. Eso no es un error en tu script — es una sola IP de salida que muere o se limita por tasa a mitad de ejecución. La solución es reintentar con rotación: envuelve la llamada, captura ConnectionError, y deja que un gateway rotativo te entregue una IP nueva en el siguiente intento. A través de proxies rotativos cada reintento viaja por una salida diferente, por lo que una dirección muerta nunca puede fallar una solicitud dos veces:

import requests

def get_with_rotation(url, proxies, attempts=4):
    last = None
    for _ in range(attempts):
        try:
            r = requests.get(url, proxies=proxies, timeout=(5, 20))
            if r.status_code not in (429, 500, 502, 503, 504):
                return r
            last = r.status_code
        except requests.exceptions.ConnectionError as e:  # Proxy/SSL/ConnectTimeout
            last = e
    raise RuntimeError(f"failed after {attempts} attempts: {last}")

Si ProxyError persiste a través de muchas IPs nuevas, el problema se ha movido de tu grupo al objetivo: estás siendo bloqueado, no desconectado. Esa es una lucha diferente — consulta la lista de verificación anti-baneo para el ritmo, encabezados e higiene de sesión.

Hay una distinción más que vale la pena internalizar, porque cambia cómo respondes. Un ProxyError o ConnectTimeout significa que la solicitud nunca se completó, por lo que reintentarlo es seguro incluso para un POST — nada sucedió en el lado lejano. Un ReadTimeout, por el contrario, significa que el objetivo recibió tu solicitud y simplemente tardó demasiado en responder; reintentar una escritura no idempotente puede duplicar el envío. Cuando construyas el bucle de reintento, trata los fallos en la etapa de conexión como libremente reintentables y los fallos en la etapa de lectura como reintentables solo para GET y HEAD. Esa única regla previene el error sutil donde un proxy inestable convierte un pago en tres.

Lista de verificación que asocia mensajes de error comunes de proxy de requests de Python con sus soluciones de una línea, desde no se puede conectar al proxy hasta fallos intermitentes
Lee el mensaje, aplica la solución: host incorrecto, esquema faltante, clave TLS incorrecta, contraseña no codificada o una salida moribunda, cada uno se asocia a un paso correctivo único.

Preguntas frecuentes

¿Qué causa requests.exceptions.ProxyError: no se puede conectar al proxy?

El host o puerto del proxy es incorrecto, el proxy está caído o un firewall está bloqueando la conexión antes de que salga cualquier solicitud. Verifica el endpoint con curl -x usando las mismas credenciales; si curl también falla, el proxy es inalcanzable, y si curl tiene éxito, una variable de entorno o un diccionario mal formado en tu Python es el culpable.

¿Por qué ProxyError está envuelto en HTTPSConnectionPool?

Ese envoltorio solo nombra el pool de conexiones que urllib3 usó para alcanzar el objetivo — es ruido alrededor del mensaje real anidado dentro. Lee la cláusula Caused by más interna: Cannot connect to proxy significa un fallo de conexión, mientras que un mensaje 407 o SSL dentro del mismo pool apunta a un problema de autenticación o de TLS/esquema en su lugar.

¿Cómo detengo que requests lea proxies del entorno?

Configura session.trust_env = False en tu Session, o pasa trust_env=False de manera equivalente, para que Requests ignore HTTP_PROXY y HTTPS_PROXY. Esta es la solución cuando un proxy funciona en un shell pero lanza ProxyError en otro, o cuando una variable corporativa secuestra un scraper que no configuraste para usar un proxy.

¿Es seguro reintentar ConnectTimeout?

Sí — la documentación de Requests marca ConnectTimeout como seguro para reintentar porque la solicitud nunca llegó al servidor, por lo que no pudo haber ocurrido ningún efecto secundario. Reinténtalo, idealmente a través de un gateway rotativo para que el siguiente intento use una salida diferente y más rápida. ReadTimeout es más arriesgado de reintentar a ciegas en métodos no idempotentes como POST.

Una vez que dejas de tratar ProxyError como una falla única y comienzas a leerlo por etapa del ciclo de vida, las soluciones son mecánicas: errores tipográficos de esquema se levantan antes de la red, fallos de conexión nombran un proxy muerto, errores SSL nombran una clave incorrecta, y fallos intermitentes quieren rotación, no un reinicio. Salidas limpias eliminan la mayoría de ellos por completo.

Scrapea con IPs residenciales que se mantienen vivas