Proxy en Python requests: configuración, autenticación y errores
Cómo pasar Python Requests por un proxy HTTP o SOCKS5: diccionario proxies, usuario y contraseña, Session frente al entorno, timeouts, TLS y qué significan ProxyError, 407 y SSLError.
Esta guía es para quien necesita que Python Requests salga por un proxy y sepa qué hacer cuando el proxy falla. Los ejemplos usan hosts de laboratorio y credenciales dummy. Top Proxy no vende IP ni reparte listas gratis; compara servicios que sí lo hacen.
01Configurar el proxy y comprobar por dónde sale la petición
Cada método de Requests acepta un diccionario proxies. Las claves son el esquema de la URL destino. Los valores son la URL del proxy:
import requests
proxies = {
"http": "http://203.0.113.10:8080",
"https": "http://203.0.113.10:8080",
}
r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=(3.05, 27))
print(r.status_code, r.json())
Pasos mínimos que sí se pueden repetir:
- Instala Requests 2.34.2 o posterior en un entorno aislado.
- Monta el diccionario con las claves
httpyhttps. Si omites una, ese esquema puede ir directo. - Pasa
timeouten cada llamada. Requests no tiene timeout por defecto. - Comprueba el origen que vio el servidor, no la pestaña del navegador.
- Si el proxy pide login, mete usuario y contraseña en la URL del proxy, no en
auth=del sitio.
httpbin.org/ip devuelve JSON con origin, la dirección que vio el servidor. Compárala con el rango de salida que documenta tu plan, y anota tu dirección directa en Cuál es mi IP. Una dirección que no cambia no demuestra por sí sola que el proxy se saltó. Algunos entornos salen por la misma red en la que estás. Si solo quieres saber si un endpoint responde, usa el comprobador de proxies o la guía cómo comprobar si un proxy funciona.
La IP que muestra el navegador en "mi IP" es la salida del navegador. No prueba la ruta de tu proceso Python. La prueba de Python es la traza del proxy más el origen que vio el endpoint.
La documentación estable pide el esquema en la URL del proxy. En Requests 2.34.2 un valor 127.0.0.1:puerto sin esquema también funcionó: requests.utils.prepend_scheme_if_needed lo convierte a http://127.0.0.1:puerto. En el fixture, HTTP al origen local devolvió 200 con ese host:puerto contra un proxy abierto. El HTTPS de laboratorio usó CONNECT a localhost y la CA de prueba, no un sitio público. Sigue escribiendo http://. El arreglo no sustituye un esquema socks5h:// ni un proxy que termina TLS en su propio puerto.
Una clave scheme://hostname enruta un host exacto distinto:
proxies = {
"https": "http://203.0.113.10:8080",
"https://api.internal.example": "http://198.51.100.5:3128",
}
Esa coincidencia es exacta. No aplica a todos los nombres bajo el mismo dominio.
02Distinguir el proxy HTTP del destino HTTPS
La clave "https" casi siempre apunta a una URL de proxy http://. Es correcto: el cliente pide al proxy un túnel CONNECT y el TLS corre dentro. Usa https:// ahí solo si el proveedor termina TLS en el puerto del proxy.
En el laboratorio, un GET HTTP al origen local sin credenciales recibió 407 en el cuerpo. El mismo proxy, sin credenciales, contra el origen TLS local levantó ProxyError con Tunnel connection failed: 407 Proxy Authentication Required sobre un CONNECT localhost. No son el mismo síntoma. Un usuario dummy incorrecto también produjo 407 en HTTP y ProxyError 407 en CONNECT. El 407 de un GET HTTP es la respuesta del proxy. El 407 de HTTPS suele ir envuelto porque el CONNECT no llegó a establecerse.
El protocolo del proxy no es el origen de la IP. Un HTTP 200 por un datacenter, un residencial o un móvil usa el mismo diccionario proxies. El tipo de salida se elige en el catálogo, residencial, móvil o datacenter, y se comprueba en el destino real. Un 200 de laboratorio no otorga geografía, elegibilidad de cuenta ni acceso a un sitio concreto.
03Pasar usuario y contraseña sin dejarlos en el código
La mayoría de los proxies de pago piden login. Requests admite autenticación Basic del proxy en la propia URL, http://usuario:contraseña@host:puerto, en el diccionario o en las variables de entorno:
import os
from urllib.parse import quote
user = quote(os.environ["PROXY_USER"], safe="")
password = quote(os.environ["PROXY_PASS"], safe="")
host = "gate.example-provider.com:7000"
proxy_url = f"http://{user}:{password}@{host}"
proxies = {"http": proxy_url, "https": proxy_url}
Carga las credenciales de un gestor de secretos en tiempo de ejecución. La documentación de Requests trata guardarlas en variables de entorno o en archivos versionados como un riesgo. Codifica usuario y contraseña con quote(..., safe=""). Requests deshace el percent-encoding de las dos partes. Un @, / o # sin codificar rompe el parseo.
Después de decodificar, el user-id de HTTP Basic no puede llevar dos puntos (RFC 7617, sección 2). La contraseña sí. Codificar : como %3A en el usuario no lo vuelve legal: get_auth_from_url lo deja otra vez como dos puntos. En este runtime, http://alice%3Abob:secret@127.0.0.1:8080 se leyó como usuario alice:bob. Un dos puntos sin codificar en el userinfo se toma como inicio de la contraseña: alice:bob:secret quedó usuario alice y contraseña bob:secret. La contraseña del fixture incluía /, # y %; codificada componente a componente, el GET HTTP y el HTTPS de ejemplo devolvieron 200.
Las banderas de país o sesión pegajosa en el usuario (user-country-us-session-abc123) son sintaxis del proveedor, no de Requests. No conceden geografía ni una cuenta por sí solas.
En respuestas antiguas aparece requests.auth.HTTPProxyAuth. Añade Proxy-Authorization al objeto de la petición. En el loopback autenticó HTTP plano (200) y falló el HTTPS con ProxyError que mencionaba 407. Encaja con una cabecera que viaja en la petición y no en el CONNECT que el proxy comprueba. Prefiere las credenciales en la URL del proxy.
Requests solo hace Basic en el proxy. NTLM o Kerberos/Negotiate piden otra librería o un proxy auxiliar local.
Un 407 amplio de diagnóstico de red no es el tema de esta página. Aquí solo cómo lo enseña Requests.
04Session, variables de entorno y trustenv
Una Session reutiliza conexiones y guarda valores por defecto:
session = requests.Session()
session.proxies.update(proxies)
session.get("https://httpbin.org/ip", timeout=10)
La documentación avisa de que session.proxies puede no hacer lo que esperas. Esos valores los pueden pisar los proxies del entorno (urllib.request.getproxies()). Un HTTPS_PROXY heredado gana en silencio. Dos formas de dejarlo explícito:
# 1. proxies en cada llamada (el arreglo documentado)
session.get(url, proxies=proxies, timeout=10)
# 2. la sesión deja de leer el entorno
session.trust_env = False
session.proxies.update(proxies)
trust_env = False también deja de leer .netrc y variables del paquete de CA como REQUESTS_CA_BUNDLE.
No es un aviso de papel. En el fixture del 2026-10-14, Session.proxies apuntaba a un proxy de loopback y HTTP_PROXY / HTTPS_PROXY a otro. El proxy del entorno recibió el GET. La misma sesión, con proxies= en la llamada, golpeó el proxy de la sesión. Una segunda sesión con trust_env = False usó Session.proxies e ignoró el entorno. Si un script "a veces" usa el proxy del CI, mira aquí antes de reescribir la URL.
Si no pasas proxies en absoluto, Requests lee http_proxy, https_proxy, no_proxy y all_proxy. Las mayúsculas también cuentan.
export HTTP_PROXY="http://203.0.113.10:8080"
export HTTPS_PROXY="http://203.0.113.10:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.example"
python my_script.py
Sin NO_PROXY, las llamadas a localhost o a servicios internos pueden ir al proxy del entorno y colgarse. NO_PROXY solo gobierna los proxies cogidos del entorno. Un host que coincide con NO_PROXY sigue yendo al proxy si pasas un proxies= explícito que lo cubre.
El fixture confirmó los dos lados. Con NO_PROXY=127.0.0.1,localhost y sin argumento proxies=, el GET fue directo al origen local. El mismo host, con proxies= explícito, pasó por el proxy que pasamos.
Hay un recorte más si el proceso parece un CGI. urllib.request.getproxies_environment ignora HTTP_PROXY en mayúsculas cuando existe REQUEST_METHOD, por el aviso CVE-2016-1000110 (cabecera Proxy: inyectada). http_proxy en minúsculas sí se sigue aplicando. Un worker WSGI o un test que exporta REQUEST_METHOD puede "perder" el proxy HTTP del entorno y no el de proxies=.
| De dónde sale el ajuste | Aplica a | Cuándo usarlo |
|---|---|---|
Argumento proxies= en la llamada | Esa llamada, por encima del entorno | Scripts que deben usar una salida concreta |
Session.proxies | Cada llamada de esa sesión, salvo que el entorno la pise | Clientes largos, mejor con trust_env = False |
HTTP_PROXY / HTTPS_PROXY / ALL_PROXY | Cada llamada de Requests que aún confía en el entorno | Herramientas de terceros y CI donde no editas el código |
NO_PROXY | Hosts que saltan el proxy de entorno (proxies= explícito aún puede cubrirlos) | Localhost, metadatos, APIs internas |
05SOCKS5, socks5h y dónde se resuelve el DNS
El soporte SOCKS es un extra opcional que instala PySocks (1.7.1 en este runtime):
python -m pip install "requests[socks]"
Después, las URL SOCKS entran en el mismo diccionario, credenciales incluidas:
proxies = {
"http": "socks5h://user:pass@203.0.113.10:1080",
"https": "socks5h://user:pass@203.0.113.10:1080",
}
Fíjate en la h. Con socks5:// la resolución DNS ocurre en tu máquina y al proxy solo llega la IP. Con socks5h:// el proxy resuelve el nombre. Es el mismo corte que usa curl. La documentación estable de Requests lo describe así.
El fixture lo hizo visible sin salir a Internet. Un GET HTTP a 127.0.0.1 con socks5:// devolvió 200 y el proxy SOCKS vio ATYP IPv4. Un hostname de laboratorio que no resuelve en local, lab-origin.invalid, con socks5:// falló en el cliente antes de hablar con el proxy: no hubo evento SOCKS. El mismo hostname con socks5h:// llegó al proxy como ATYP hostname y el laboratorio lo mapeó al origen local, HTTP 200. Usa socks5h cuando no quieres que tu resolver local vea esas consultas. La comparación de catálogo está en SOCKS5 frente a proxies HTTP.
| Esquema de URL | Extra | Quién resuelve el DNS | Uso habitual |
|---|---|---|---|
http:// | Ninguno | El proxy, para el host que pides | Pasarelas HTTP(S) de pago |
https:// | Ninguno | El proxy | Proveedores que terminan TLS en el puerto del proxy |
socks5:// | requests[socks] | Tu máquina | SOCKS cuando el DNS local te vale |
socks5h:// | requests[socks] | El proxy | SOCKS cuando las consultas deben quedar del lado del proxy |
Si no sabes qué habla un endpoint, pásalo por el comprobador de proxies.
06Timeouts, TLS y qué excepción sale en cada fallo
Requests no tiene timeout por defecto. Un proxy colgado puede dejar la llamada minutos. Pasa siempre timeout, mejor como tupla (connect, read):
r = requests.get(url, proxies=proxies, timeout=(3.05, 27))
El valor de conexión cubre el TCP, que con proxy es la conexión al proxy. La documentación sugiere un número un poco por encima de un múltiplo de 3, alineado con la retransmisión TCP. El valor de lectura es el hueco máximo entre bytes del servidor, no un techo de reloj de pared para toda la descarga. Ni connect ni read son un deadline total. Si el nombre tiene IPv4 e IPv6, urllib3 puede probar direcciones en serie y el tiempo percibido se alarga.
Cuando el proxy no acepta el TCP a tiempo, Requests 2.34.2 levantó ProxyError envolviendo ConnectTimeoutError de urllib3, no un ConnectTimeout de primer nivel. En el fixture, un listener local con la cola llena y timeout=(0.8, 5) produjo ese envoltorio, sin blackhole de Internet. Con un proxy que ya había conectado, el origen envió cabeceras y retrasó el cuerpo cuatro segundos. Con timeout=(3.05, 1.0) la cadena llevó ReadTimeoutError y el texto Read timed out. Captura ProxyError y errores de lectura antes de un ConnectionError genérico, y mira __cause__ si necesitas separar "el proxy no contestó" de "el proxy contestó y el cuerpo no llegó".
Para fallos transitorios, monta un adaptador con reintentos acotados:
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 502, 503, 504])
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))
POST no está en allowed_methods: no se reintenta por esos estados ni por errores de lectura, aunque un fallo de conexión previo al envío sí puede reintentarse. Los códigos 413, 429 y 503 permiten activar un reintento por Retry-After; si otro estado ya es reintentable por status_forcelist, su cabecera también puede determinar la espera. Eso sale de inspeccionar urllib3 2.8.0 en este runtime, 17 ramas, sin medir un sleep de Retry-After. El adaptador por defecto de Requests lleva max_retries.total == 0. Los reintentos no arreglan un 407, una CA mala ni un trust_env apuntando al gateway equivocado.
Ordena los except. ProxyError, SSLError y ConnectTimeout heredan de ConnectionError, y ConnectTimeout también es un Timeout. Los específicos van primero:
from requests.exceptions import (
ProxyError, SSLError, ConnectTimeout, ReadTimeout,
ConnectionError, InvalidProxyURL, RequestException,
)
try:
r = session.get(url, proxies=proxies, timeout=(3.05, 27))
r.raise_for_status()
except ProxyError as e:
... # proxy inalcanzable, CONNECT rechazado, o timeout de conexión al proxy; busca 407 en el mensaje
except SSLError as e:
... # certificado; arregla la confianza, no apagues la verificación
except ConnectTimeout:
... # no conectó a tiempo, más habitual sin proxy
except ReadTimeout:
... # conectó, el servidor se calló
except InvalidProxyURL:
... # URL de proxy mal formada
except ConnectionError:
... # DNS, reset, otras redes
except RequestException:
... # el resto de Requests
| Síntoma | Causa probable | Qué probar |
|---|---|---|
ProxyError sin 407 en el mensaje | Nadie acepta host/puerto, proxy caído, red o allowlist, o timeout de conexión al proxy | Host y puerto, luego el comprobador; las credenciales no son el primer sospechoso |
ProxyError que menciona 407 en una URL HTTPS | El CONNECT no llevó credenciales que el proxy aceptara: no se enviaron, solo HTTPProxyAuth, mal codificadas, formato distinto o plan inactivo | Credenciales codificadas en la URL del proxy; confirma formato de usuario y estado del plan |
| Respuesta 407 en una URL HTTP | El proxy pide credenciales; sin ellas es solo el desafío | Añádelas a la URL; si ya estaban, revisa encoding y formato |
| Respuesta 401 | El destino, no el proxy, pide autenticación | Arregla el auth del sitio; el tramo del proxy funcionó |
Requests ignora tu Session.proxies | Los proxies del entorno lo pisan | proxies= por llamada o trust_env = False |
InvalidSchema: Missing dependencies for SOCKS support | Falta requests[socks] | python -m pip install "requests[socks]" |
SSLError: CERTIFICATE_VERIFY_FAILED | Proxy que intercepta TLS o un certificado de laboratorio que el bundle por defecto no confía | verify= o REQUESTS_CA_BUNDLE al archivo CA de ese proxy |
| Se cuelga sin fin | No hay timeout | Pasa siempre una tupla (connect, read) |
origin no coincide con la salida esperada | No se aplicó el proxy a ese esquema, NO_PROXY coincidió mientras confiabas en el entorno, u otra ruta | Pon http y https; si usas entorno, revisa NO_PROXY; compara con el rango documentado del proveedor |
Lo que mostró la prueba local

Los fragmentos apuntan a Requests 2.34.2 en PyPI, Python 3.10 o posterior, y a la documentación avanzada de Requests. El 2026-10-14 el laboratorio corrió en un proceso hijo con entorno limpio: sin HTTP_PROXY/HTTPS_PROXY/ALL_PROXY heredados, sin REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE, sin REQUEST_METHOD, HOME temporal sin netrc y un ftp_proxy centinela para que macOS no lea el proxy del sistema. Runtime: Python 3.14.6, urllib3 2.8.0, certifi 2026.07.22, PySocks 1.7.1, TLS verificado. 29 casos de loopback capturados. 30 aserciones en total, incluida default_adapter_retries_total_zero del adaptador, que no es una ruta. 17 comprobaciones instaladas de urllib3.util.retry.Retry, constantes y ramas; no se midió espera de Retry-After. Usuario dummy demo. Contraseña con puntuación, percent-encoded, redactada. HTTPS solo origen local con CA de laboratorio. Eso no es velocidad, geografía ni calidad de IP de un proveedor.
| Caso | Destino HTTP | Destino HTTPS local |
|---|---|---|
| Sin credenciales | respuesta 407 en GET | ProxyError 407 en CONNECT |
| Usuario dummy incorrecto | 407, cabecera presente, no autorizado | ProxyError 407 en CONNECT |
| Credenciales percent-encoded en la URL | 200 | 200, CONNECT autenticado, verify= a la CA |
host:port sin esquema, proxy abierto | 200 GET | no mezclado con HTTPS público |
Solo HTTPProxyAuth | 200 | ProxyError 407, CONNECT sin Proxy-Authorization |
| Puerto de proxy cerrado | ProxyError + NewConnectionError | no aplicado |
| Auth de proxy válida, el sitio devuelve 401 | 401 del sitio | no aplicado |
| CA por defecto frente a cert de lab | n/a | SSLError / SSLCertVerificationError; 200 al confiar la CA |
HTTP_PROXY en mayúsculas | GET al proxy de entorno | no aplicado |
REQUEST_METHOD + HTTP_PROXY mayúsculas | GET directo, CGI omite el proxy HTTP | no aplicado |
REQUEST_METHOD + http_proxy minúsculas | GET al proxy de entorno | no aplicado |
Session.proxies frente al entorno | el proxy de entorno recibió el GET | no aplicado |
proxies= en la llamada | el proxy de la sesión recibió el GET | no aplicado |
trust_env = False | usó Session.proxies | no aplicado |
NO_PROXY coincide | directo al origen | no aplicado |
proxies= explícito con NO_PROXY | igual se proxifica | no aplicado |
| Cola local llena, connect timeout | ProxyError + ConnectTimeoutError | no aplicado |
| Cuerpo retrasado tras cabeceras | ReadTimeoutError en la cadena | no aplicado |
| Baseline sin proxy | 200, ningún proxy vio el GET | no aplicado |
SOCKS: IPv4 hacia 127.0.0.1 con socks5://. Hostname de laboratorio solo con socks5h://. socks5:// contra un nombre que no resuelve no llegó al proxy.
Un proxy corporativo que inspecciona TLS o un interceptador local vuelve a firmar HTTPS con su CA, y Requests la rechaza. Confía esa CA. No apagues la verificación:
r = requests.get(
url,
proxies={"https": "http://127.0.0.1:8080"},
verify="/ruta/a/proxy-ca.pem",
timeout=10,
)
o export REQUESTS_CA_BUNDLE=/ruta/a/ca.pem para el proceso. Pasa un DER a PEM antes. El origen autofirmado del lab reprodujo la misma clase de fallo: verify=True por defecto levantó SSLError (certificate verify failed: self-signed certificate); la misma petición con verify= a la CA del lab devolvió 200. verify=False hace que Requests acepte cualquier certificado e ignore el hostname. No es un arreglo normal de un proxy que "no funciona".
07Elegir un endpoint compatible y verificar el resultado
Muchos proveedores rotan detrás de un solo host de gateway. Según la tarifa, la salida cambia por petición o en un intervalo de sesión, y un session ID en el usuario suele clavar una salida un rato en lugar de dar una IP nueva a cada llamada. Esas reglas son del proveedor. Proxy rotativo y sesiones sticky cubre el lenguaje de producto. Si tú gestionas una lista, elige un endpoint por petición y pásalo en proxies=:
import itertools
pool = itertools.cycle(
[
"http://user:pass@203.0.113.10:8080",
"http://user:pass@203.0.113.11:8080",
]
)
for url in urls:
p = next(pool)
session.get(url, proxies={"http": p, "https": p}, timeout=(3.05, 27))
Pasa el proxy por llamada, porque el entorno puede pisar Session.proxies. El tipo de salida es otra decisión: datacenter, residencial o móvil. Precio y cómo trata el destino el tráfico dependen de la tarifa, del destino y de la reputación de la IP, no de la etiqueta sola. Un proxy no concede geografía ni elegibilidad de cuenta por sí mismo. La diferencia de origen está en residenciales frente a datacenter.
Antes de contratar, lee la ficha actual del proveedor para host, puerto, esquema y formato de usuario. Un HTTP 200 de la ficha en el catálogo no prueba que ese plan hable HTTP CONNECT, SOCKS5 o Basic del modo que usa Requests. El 2026-10-14 la guía rápida residencial de Bright Data describía alta de zona y KYC, no un snippet de proxies= medido aquí. Oxylabs, Decodo y Smartproxy no se ejecutaron contra este laboratorio. No hay prueba de éxito, geografía ni cuenta.
Las fichas siguientes están publicadas en español en el catálogo. Compáralas. No las medimos:

Bright Data es una gran red de proxies para recopilar datos: IP residenciales, móviles, ISP y de datacenter en unos 195 países. El tráfico residencial se puede pagar por uso sin suscripción, y las direcciones de datacenter e ISP se venden en paquetes desde 10 IP. El acceso residencial y móvil exige verificar la identidad.

Oxylabs vende proxies residenciales, de datacenter y móviles. El residencial de autoservicio empieza en $6/GB para 5 GB/mes. Pagos del comprador: tarjetas, transferencia, AliPay, PayPal.

SOAX vende proxies residenciales y móviles rotativos en un pool compartido de créditos, con datacenter e ISP en los mismos planes. Builder empieza en $200/mes + IVA (200 créditos); el tráfico Tier 1 es $3/GB en Builder.

Decodo (antes Smartproxy) vende proxies residenciales y de datacenter. Residencial: $4/GB sin plan mensual, o paquetes mensuales desde $3.75/GB por 3 GB. Pagos: tarjetas, PayPal, Alipay, Google/Apple Pay, cripto (con límites).
¿Cómo usar un proxy con Python requests?
¿Cómo añado usuario y contraseña al proxy de requests?
¿Por qué Python requests devuelve 407 Proxy Authentication Required?
¿Requests admite proxies SOCKS5?
¿Requests usa la variable HTTP_PROXY?
¿Qué timeout pongo a requests por un proxy?
¿Debo poner verify=False si el proxy rompe HTTPS?
Artículos relacionados
Proxy con curl: configuración, autenticación y errores
Configura curl con un proxy HTTP o SOCKS5, separa -U de -u, lee CONNECT, http_proxy y los códigos 407, 7, 28 y 60.
Gemini Developer API: configurar un proxy HTTP y diagnosticar la conexión
Enruta google-genai 2.30.1 por HTTPS_PROXY y CONNECT, conserva TLS con SSL_CERT_FILE y separa un fallo de transporte de un 403 o un timeout.
Configurar un proxy en HTTPX: autenticación, entorno y tiempos de espera
Configura proxy= en HTTPX 0.28.1, separa las credenciales del proxy y del sitio, deja HTTPS en CONNECT y distingue entorno, 407 y ReadTimeout.



