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:
- ProxyError — se lanza cuando la conexión al proxy falla: host inalcanzable, puerto incorrecto, conexión rechazada o un
407rechazado. El mensaje a menudo anidaCannot connect to proxydentro de un envoltorioHTTPSConnectionPool(...). - SSLError — el proxy se conectó, pero el apretón de manos TLS al objetivo falló. En configuraciones de proxy, el desencadenante usual es
https://escrito dentro de la clavehttpsen lugar dehttp://. - ConnectTimeout — el proxy no respondió dentro de la ventana de conexión. Es subclase tanto de
ConnectionErrorcomo deTimeout, y está documentado explícitamente como seguro para reintentar. - ReadTimeout — el proxy se conectó y reenvió la solicitud, pero el objetivo fue demasiado lento para responder. Este es un problema de calidad del objetivo o de la salida, no un error de configuración.
- MissingSchema / InvalidProxyURL — se lanza antes de cualquier llamada de red cuando la URL del proxy está mal formada. Son errores tipográficos puros.
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",
}

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.

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.