Si estás raspando sitios o automatizando solicitudes a través de proxies en Python, tarde o temprano te encontrarás con tres
errores clásicos: ProxyError,
SSLError: WRONG_VERSION_NUMBER y HTTP
407 Proxy Authentication Required. En este artículo analizaremos
la causa de cada uno de ellos y mostraremos código funcional para solucionarlos.
Cómo requests trabaja con proxies: sintaxis básica
La biblioteca requests acepta proxies a través de un diccionario
proxies, donde las claves son el esquema de la solicitud
(http y https),
y los valores son las URL del servidor proxy con autorización opcional.
import requests
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())
Ten en cuenta: incluso para solicitudes HTTPS, el esquema del proxy generalmente se indica como
http://, porque la mayoría de los servidores proxy aceptan
un túnel CONNECT a través del protocolo HTTP, y no lo envuelven en SSL. Si especificas
https:// para un proxy que no lo soporta, obtendrás
precisamente un SSLError, cuyo análisis se presentará a continuación.
Si trabajas con proxies SOCKS5, necesitarás un paquete adicional:
pip install requests[socks]
# Y la sintaxis de proxies cambia:
proxies = {
"http": "socks5://user:password@proxy_host:port",
"https": "socks5://user:password@proxy_host:port",
}
ProxyError: causas y soluciones
requests.exceptions.ProxyError es un envoltorio sobre excepciones
de red de bajo nivel que ocurren al intentar establecer una conexión con un servidor proxy. Los cuatro escenarios más comunes son:
1. Proxy no disponible o dirección/puerto incorrectos
requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.',
ConnectionResetError(104, 'Connection reset by peer')))
Verifica que el host y el puerto estén ingresados correctamente y que el servidor proxy esté realmente activo. La forma más rápida de diagnosticar es
intentar conectarse a través de curl:
curl -x http://user:password@proxy_host:port https://httpbin.org/ip -v
2. El proxy bloqueó tu solicitud (baneo por IP o User-Agent)
Los proxies de centros de datos a menudo son bloqueados por los sitios objetivo debido a que sus rangos de IP son conocidos por los sistemas antifraude. Si el error aparece de manera consistente al acceder a un sitio específico (por ejemplo, Wildberries, Ozon o redes sociales), y no a todos los sitios a la vez, el problema no está en el proxy en sí, sino en su reputación. En este caso, cambiar a proxies residenciales puede ayudar, ya que utilizan IP de usuarios reales y son menos propensos a ser bloqueados por sistemas antifraude.
3. Límite de conexiones simultáneas excedido
Si estás ejecutando un raspado multihilo y abriendo cientos de conexiones a través del mismo proxy, el servidor puede
comenzar a cerrar conexiones adicionales. La solución es utilizar un pool de conexiones a través de
requests.adapters.HTTPAdapter con un límite de
pool_maxsize y
max_retries:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retries, pool_connections=20, pool_maxsize=20)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
4. Timeout en el servidor proxy
Si el proxy "se queda colgado" más allá del timeout establecido, verás un
ProxyError envuelto en
ReadTimeoutError. Aumenta el timeout a un valor razonable
(generalmente 15-30 segundos es suficiente) y agrega el manejo de excepciones por separado:
try:
response = requests.get(url, proxies=proxies, timeout=20)
except requests.exceptions.ProxyError as e:
print(f"Problema con el proxy: {e}")
except requests.exceptions.Timeout:
print("El proxy no respondió a tiempo")
SSLError WRONG_VERSION_NUMBER: análisis del problema
Este error es uno de los más confusos a primera vista, pero en realidad casi siempre tiene una sola causa.
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL:
WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)')))
WRONG_VERSION_NUMBER significa que el cliente intentó realizar
un handshake SSL/TLS donde el servidor espera tráfico HTTP normal (sin cifrado). La causa más común radica en un esquema de proxy incorrecto
especificado en el diccionario proxies.
Error común: especificar https:// en lugar de
http:// en la dirección del servidor proxy. La mayoría de los proxies HTTP(S)
aceptan conexiones a través de HTTP normal, incluso cuando están proxyando tráfico HTTPS a través del mecanismo CONNECT.
# INCORRECTO — causará WRONG_VERSION_NUMBER
proxies = {
"http": "https://user:password@proxy_host:port",
"https": "https://user:password@proxy_host:port",
}
# CORRECTO
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
La segunda causa común es un puerto que físicamente no soporta TLS. Por ejemplo, si el proveedor de proxy asignó el puerto 8080 solo para tráfico HTTP, y estás intentando establecer una conexión TLS directamente (saltando CONNECT), el servidor responderá con texto normal en lugar de un handshake TLS, y la biblioteca cliente OpenSSL generará este error. Verifica en la documentación de tu proveedor de proxy qué puertos y protocolos (HTTP/HTTPS/SOCKS5) están disponibles en cada puerto específico.
La tercera opción es que estés utilizando socket o bibliotecas
externas para una conexión TCP directa con el proxy, saltando el mecanismo estándar de requests. En este caso, asegúrate de que
la envoltura TLS se aplique solo al sitio objetivo real, y no a la propia conexión con el proxy.
407 Proxy Authentication Required: autorización
El código de respuesta 407 es análogo al estándar 401, pero para servidores proxy. Significa que el servidor requiere autorización, y tú no la has proporcionado o la has proporcionado en un formato incorrecto.
Método 1: nombre de usuario y contraseña directamente en la URL
proxies = {
"http": "http://login:password@proxy_host:port",
"https": "http://login:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies)
Este método es el más simple, pero si el nombre de usuario o la contraseña contienen caracteres especiales (por ejemplo, @,
:, /),
deben ser escapados a través de urllib.parse.quote:
from urllib.parse import quote
login = quote("user@domain")
password = quote("p@ssw:ord!")
proxy_url = f"http://{login}:{password}@proxy_host:port"
proxies = {"http": proxy_url, "https": proxy_url}
Método 2: encabezado Proxy-Authorization manualmente
Si el proveedor de proxy utiliza un esquema de autorización no estándar o necesitas más control, puedes construir el encabezado manualmente a través de codificación base64:
import base64
import requests
login = "user"
password = "password"
credentials = base64.b64encode(f"{login}:{password}".encode()).decode()
headers = {
"Proxy-Authorization": f"Basic {credentials}"
}
proxies = {
"http": "http://proxy_host:port",
"https": "http://proxy_host:port",
}
response = requests.get(
"https://httpbin.org/ip",
proxies=proxies,
headers=headers,
timeout=10
)
Importante: algunos proveedores de proxy vinculan la autorización no al nombre de usuario/contraseña, sino a una IP estática específica del cliente (whitelist). Si cambias de IP (por ejemplo, trabajas a través de VPN o IP dinámica del proveedor), el error 407 puede aparecer incluso con credenciales correctas, porque tu IP externa actual no está en la lista blanca del proxy. La solución es acceder a la cuenta del proveedor y actualizar la lista de IP permitidas, o cambiar a autorización por nombre de usuario/contraseña en lugar de IP-whitelist.
Lógica de reintentos y manejo de excepciones
En la práctica, los errores de proxy rara vez son únicos, especialmente durante el raspado masivo. La arquitectura correcta del código debe prever reintentos con retraso y registro de cada error por separado.
import time
import requests
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_with_retry(url, proxies, max_retries=3, backoff=2):
for attempt in range(1, max_retries + 1):
try:
response = requests.get(url, proxies=proxies, timeout=15)
response.raise_for_status()
return response
except requests.exceptions.ProxyError as e:
logger.warning(f"[Intento {attempt}] ProxyError: {e}")
except requests.exceptions.SSLError as e:
logger.warning(f"[Intento {attempt}] SSLError: {e}")
except requests.exceptions.HTTPError as e:
if response.status_code == 407:
logger.error("407: verifica el nombre de usuario/contraseña o la whitelist de IP del proxy")
break
logger.warning(f"[Intento {attempt}] HTTPError: {e}")
except requests.exceptions.Timeout:
logger.warning(f"[Intento {attempt}] Timeout de conexión")
time.sleep(backoff * attempt)
raise RuntimeError(f"No se pudo obtener {url} después de {max_retries} intentos")
Este enfoque ahorra horas de depuración: en lugar de que el script falle en el primer problema de red, obtienes un registro claro que indica el tipo específico de error en cada intento.
Rotación de proxies y lucha contra bloqueos
Si ProxyError y 407 están más relacionados con la configuración del código, los bloqueos masivos y las negativas estables de conexión en dominios específicos casi siempre indican un problema con la calidad del propio pool de IP. Para tareas como el raspado de marketplaces (Wildberries, Ozon, Avito) o trabajo con redes sociales, se recomienda rotar IP en cada solicitud o a través de un intervalo establecido.
import random
proxy_pool = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_random_proxy():
proxy = random.choice(proxy_pool)
return {"http": proxy, "https": proxy}
response = requests.get(url, proxies=get_random_proxy(), timeout=15)
Para parsers en producción, generalmente es más conveniente utilizar proxies con rotación automática en el lado del proveedor, así no es necesario mantener una lista de IP por tu cuenta, la rotación ocurre de manera transparente a través de un solo endpoint. En este caso, los proxies residenciales son especialmente adecuados para eludir la protección anti-bot en sitios con sistemas de detección estrictos, mientras que los proxies de centros de datos son una buena opción si la velocidad de las solicitudes es importante y el sitio objetivo no bloquea agresivamente los rangos de centros de datos.
| Tipo de proxy | Velocidad | Riesgo de baneo | Tareas |
|---|---|---|---|
| Centros de datos | Alta | Medio-alto | Raspado rápido de APIs abiertas |
| Residenciales | Media | Bajo | Raspado de marketplaces, redes sociales |
| Móviles | Media-baja | Mínimo | Trabajo con sistemas anti-bot estrictos |
Session, timeouts y verificación SSL
Para scripts de larga duración (por ejemplo, parsers cíclicos), es recomendable utilizar
requests.Session() en lugar de llamadas individuales
requests.get(). Session reutiliza conexiones TCP,
lo que reduce la carga en el servidor proxy y disminuye la cantidad de ProxyError en grandes volúmenes de solicitudes.
session = requests.Session()
session.proxies.update({
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
})
for url in urls_list:
try:
resp = session.get(url, timeout=15)
print(resp.status_code, url)
except requests.exceptions.RequestException as e:
print(f"Error en {url}: {e}")
También es importante mencionar el parámetro verify. Si trabajas
con un proxy que cambia el certificado SSL (proxy MITM para depuración o algunos proxies corporativos),
requests puede quejarse de la discrepancia del certificado. Desactivar la verificación completamente
(verify=False) solo debe hacerse en un entorno de prueba, ya que en producción
esto crea un riesgo de ataques "hombre en el medio":
# Solo para depuración local!
response = requests.get(url, proxies=proxies, verify=False, timeout=15)
# La forma correcta es especificar la ruta al certificado de proxy de confianza
response = requests.get(url, proxies=proxies, verify="/path/to/proxy_cert.pem")
Lista de verificación para diagnóstico de errores
Si el script falla con un error de proxy, revisa esta lista de arriba a abajo: en el 90% de los casos, la causa se encontrará en los primeros tres puntos.
- Verifica el esquema en el diccionario
proxies— casi siempre debe serhttp://, incluso para solicitudes HTTPS. - Prueba el proxy directamente a través de
curl -x, para descartar problemas en el lado del código. - Asegúrate de que el nombre de usuario y la contraseña no contengan caracteres especiales no escapados en la URL.
- Verifica si el proxy está vinculado a una IP específica (whitelist) en lugar de autorización por nombre de usuario/contraseña.
- Aumenta el
timeouta 15-30 segundos antes de considerar que el proxy no está funcionando. - Si el error aparece solo en un sitio específico, el problema está en la reputación de la IP, no en el código.
- Agrega lógica de reintentos con retraso exponencial en lugar de intentos únicos.
- Para raspado masivo, utiliza rotación de IP, no un solo proxy estático para todas las solicitudes.
Conclusión
La mayoría de los errores de proxy en Python requests se reducen a tres cosas: un esquema incorrecto en la URL del proxy
(http en lugar de https),
falta o formato incorrecto de autorización, y la calidad del propio pool de IP. ProxyError generalmente indica un problema
de conexión o baneo de IP, SSLError WRONG_VERSION_NUMBER casi siempre señala un esquema de proxy incorrecto, y 407 —
un problema con el nombre de usuario/contraseña o la whitelist.
Si te enfrentas regularmente a bloqueos y errores 407 al raspar marketplaces o redes sociales, es recomendable revisar la fuente del proxy en sí, no solo el código. Para un raspado estable con un bajo porcentaje de bloqueos, recomendamos probar proxies residenciales — utilizan direcciones IP reales y generan significativamente menos ProxyError en sitios protegidos. Si la velocidad y el volumen de solicitudes son la prioridad, considera proxies de centros de datos — son más baratos y más rápidos para tareas de raspado simples.