httpx es el cliente HTTP al que recurren la mayoría de los nuevos proyectos en Python cuando superan las limitaciones de requests, principalmente porque ofrece una única API tanto para código síncrono como asíncrono. Esta guía muestra cómo integrar un proxy httpx tanto en Client como en AsyncClient, autenticarse mediante un punto final residencial rotativo, fijar una sesión persistente, añadir SOCKS5, ajustar los tiempos de espera y los reintentos, y diagnosticar los tres errores que realmente se pueden encontrar. Al final, dispondrás de un cliente reutilizable que se conecta a través de un proxy y puede demostrar su IP de salida.
Los ejemplos están pensados para httpx 0.27 o versiones posteriores. Si utilizas una versión anterior, ten en cuenta que la palabra clave proxies= pasó a llamarse proxy= y mounts= en httpx 0.26; todo lo que se indica a continuación utiliza los nombres actuales.
Dos formas de configurar un proxy httpx: proxy y mounts
httpx te ofrece una opción sencilla y otra precisa. proxy= toma una única URL y redirige todas las solicitudes a través de ella. mounts= asigna patrones de URL a transportes, y cada transporte puede tener su propio proxy, número de reintentos y límites de conexión.
| Opción | Ámbito | Cuándo utilizarla |
|---|
proxy="http://..." | Todo el cliente, todos los esquemas | Un único proxy para todo, que es lo habitual con una pasarela rotativa |
mounts={"https://": HTTPTransport(proxy=...)} | Por patrón de URL | Diferentes proxies por esquema o host, o sin pasar por el proxy para servicios internos |
httpx.get(url, proxy=...) | Llamada única de nivel superior | Comprobaciones rápidas y scripts sin un cliente de larga duración |
La forma simple tiene este aspecto:
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
with httpx.Client(proxy=PROXY, timeout=20.0) as client:
r = client.get("https://api.ipify.org?format=json")
print(r.status_code, r.json())
El puerto 9000 es la puerta de enlace rotativa HTTP(S), por lo que cada nueva conexión obtiene una IP residencial diferente. Las credenciales se incluyen en la URL como autenticación básica, y httpx las convierte en un encabezado Proxy-Authorization sin necesidad de código adicional.
La trampa del enrutamiento por esquema
Con mounts puedes asignar transportes distintos a los destinos http:// y https://. La trampa está en el esquema de la propia URL del proxy. Para un destino https://, httpx envía CONNECT al proxy y, a continuación, negocia TLS con el origen dentro de ese túnel. El salto al proxy es HTTP sin cifrar, por lo que la URL del proxy en el montaje https:// debería comenzar normalmente por http://, no por https://. Al escribir https:// ahí, se le indica a httpx que envuelva el propio salto al proxy en TLS, algo que la mayoría de las pasarelas no admiten, y se produce un error de establecimiento de conexión que parece no tener relación con los proxies.
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
mounts = {
"http://": httpx.HTTPTransport(proxy=PROXY),
"https://": httpx.HTTPTransport(proxy=PROXY), # http:// scheme is correct here
# internal services stay direct: mount a transport without a proxy
"all://*.internal.example": httpx.HTTPTransport(),
}
with httpx.Client(mounts=mounts) as client:
print(client.get("https://api.ipify.org?format=json").json())
Los patrones más específicos prevalecen sobre los generales, por lo que el montaje all://*.internal.example omite el proxy para ese dominio, mientras que todo lo demás se enruta.
Autenticación en la URL
Las credenciales Geonode consisten en un nombre de usuario y una contraseña sin cifrar, por lo que el formulario de información de usuario http://user:pass@host:port es todo lo que necesitas. Dos notas prácticas. En primer lugar, si la contraseña contiene caracteres como @, : o /, codifícalos con el código de porcentaje urllib.parse.quote antes de construir la URL. En segundo lugar, mantén los datos confidenciales fuera del control de código fuente leyéndolos desde el entorno:
import os
from urllib.parse import quote
import httpx
user = os.environ["GEONODE_USER"]
password = quote(os.environ["GEONODE_PASS"], safe="")
proxy = f"http://{user}:{password}@premium-residential.geonode.com:9000"
client = httpx.Client(proxy=proxy)
Todo lo que viene a continuación añade opciones de segmentación y de sesión ampliando el nombre de usuario, no la URL, por lo que el mismo patrón sigue funcionando.
Sincronización entre Client y AsyncClient
El cliente asíncrono utiliza la misma palabra clave proxy=. La única diferencia aparece al pasar a mounts: AsyncClient requiere httpx.AsyncHTTPTransport, no httpx.HTTPTransport. Al pasar el transporte sincrónico a un cliente asíncrono, se produce un error en la primera solicitud.
import asyncio
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
async def fetch(client: httpx.AsyncClient, url: str) -> str:
r = await client.get(url)
r.raise_for_status()
return r.json()["ip"]
async def main() -> None:
transport = httpx.AsyncHTTPTransport(proxy=PROXY, retries=2)
async with httpx.AsyncClient(
mounts={"all://": transport}, timeout=httpx.Timeout(10.0, connect=5.0)
) as client:
urls = ["https://api.ipify.org?format=json"] * 5
ips = await asyncio.gather(*(fetch(client, u) for u in urls))
print(ips)
asyncio.run(main())
Dado que el puerto 9000 rota por conexión, las cinco solicitudes simultáneas suelen mostrar varias direcciones IP diferentes. Ese es el comportamiento esperado de un grupo rotativo; si necesitas que coincidan, utiliza una sesión persistente (véase más abajo).
De SOCKS5 a httpx[socks]
La compatibilidad con SOCKS en una configuración de proxy httpx es opcional. Instálala con pip install "httpx[socks]", que añade el paquete socksio, y luego cambia el esquema. El puerto rotativo de SOCKS5, incluido en Geonode, es el 11000:
import httpx
PROXY = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"
with httpx.Client(proxy=PROXY) as client:
print(client.get("https://api.ipify.org?format=json").json())
Sin el complemento instalado, httpx genera un error ImportError que menciona socksio en cuanto detecta una URL de proxy socks5://. Para el scraping habitual, los puertos HTTP son más fáciles de depurar porque las respuestas 407 son legibles.
Variables de entorno y trust_env
Con el valor predeterminado trust_env=True, httpx lee HTTP_PROXY, HTTPS_PROXY, ALL_PROXY y NO_PROXY y los aplica automáticamente. Esto resulta práctico en una estación de trabajo, pero puede ser fuente de confusión en la integración continua (CI), donde un proxy corporativo presente en el entorno reescribe silenciosamente el enrutamiento. Si configuras el proxy en el código, considera desactivar la búsqueda en el entorno para que el comportamiento sea idéntico en todas partes:
import httpx
client = httpx.Client(
proxy="http://USERNAME:PASSWORD@premium-residential.geonode.com:9000",
trust_env=False,
)
Un proxy= explícito ya tiene prioridad sobre el entorno; trust_env=False también impide que httpx lea SSL_CERT_FILE, así que configúralo deliberadamente.
Tiempos de espera y reintentos a nivel de transporte
Un proxy añade un salto, por lo que el tiempo de conexión aumenta y el tiempo de espera predeterminado de 5 segundos resulta a veces escaso para las salidas residenciales. httpx.Timeout permite establecer un presupuesto total con un límite de conexión independiente, y HTTPTransport(retries=...) reintenta los intentos de conexión fallidos incluso antes de que se envíe la solicitud.
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
transport = httpx.HTTPTransport(proxy=PROXY, retries=2)
timeout = httpx.Timeout(15.0, connect=8.0)
with httpx.Client(mounts={"all://": transport}, timeout=timeout) as client:
r = client.get("https://api.ipify.org?format=json")
print(r.json())
Los reintentos de transporte cubren los errores ConnectError y similares que se producen al establecer la conexión. No se realizan reintentos en función de los códigos de estado, ni se repiten los intentos una vez enviados los bytes; por lo tanto, un 502 procedente de la pasarela o un 429 del sitio de destino seguirá llegando a tu código. Gestiona esos casos en un pequeño bucle propio, en el que también puedes decidir si cambiar el identificador de sesión.
Sesiones persistentes con la gramática de nombre de usuario Geonode
La rotación en cada conexión no es adecuada para flujos de varios pasos, como el inicio de sesión, la paginación o la descarga. En estos casos, tu proxy httpx debe mantener la misma IP de salida en todas las solicitudes. Geonode codifica esto en el nombre de usuario y lo sirve en el puerto persistente HTTP(S) 10000:
USER[-type-residential|datacenter|mix][-country-xx][-state-name|-city-name][-asn-N][-session-XXXXXXXX][-lifetime-SECONDS]
El identificador de sesión consta de 8 caracteres alfanuméricos que usted genera; la duración es de 180 a 86 400 segundos (24 horas). No se pueden combinar el estado y la ciudad, y asn requiere un país.
import secrets
import httpx
def sticky_client(country: str = "us", lifetime: int = 600) -> httpx.Client:
session_id = secrets.token_hex(4) # 8 hex characters
username = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
proxy = f"http://{username}:PASSWORD@premium-residential.geonode.com:10000"
return httpx.Client(proxy=proxy, timeout=httpx.Timeout(15.0, connect=8.0))
with sticky_client() as client:
first = client.get("https://api.ipify.org?format=json").json()["ip"]
second = client.get("https://api.ipify.org?format=json").json()["ip"]
print(first == second, first)
Un nuevo session_id significa una nueva IP, por lo que la rotación «manual» consiste simplemente en crear un nuevo cliente. Mantén los tiempos de vida de los puertos fijos tan cortos como requiera la tarea.
Verificar la IP de salida
Todos los ejemplos anteriores acceden a https://api.ipify.org?format=json, que devuelve {"ip": "..."} independientemente de la dirección desde la que se haya accedido a él. Compáralo una vez con una llamada directa (sin proxy) y tendrás una prueba rápida de dos líneas que demuestra que el proxy está en la ruta. Haz esto antes de depurar cualquier otra cosa: muchos informes del tipo «el proxy httpx no funciona» se deben a solicitudes que nunca utilizaron el proxy porque una variable de entorno o un patrón de montaje tomaron el control.
Gestión de los códigos 407 ProxyError y ConnectError
Tres errores son los responsables de casi todos los problemas del proxy httpx. La tabla relaciona cada uno de ellos con su causa y su solución.
| Síntoma | Excepción | Causa habitual | Solución |
|---|
| Estado de respuesta 407 | httpx.ProxyError (generado durante la configuración del túnel para destinos HTTPS) o una respuesta 407 simple para destinos HTTP | Credenciales incorrectas o una opción de nombre de usuario que la pasarela rechaza | Comprueba la sintaxis del nombre de usuario; codifica la contraseña en formato porcentual |
| Túnel rechazado, 502/503 del proxy | httpx.ProxyError | La pasarela no ha podido abrir el túnel, o el puerto es incorrecto para el protocolo | Confirma que sea 9000/10000 para HTTP(S), 11000/12000 para SOCKS5 |
| No se puede acceder al servidor proxy | httpx.ConnectError | DNS, cortafuegos o nombre de host incorrecto | Resuelve el host manualmente; aumenta el tiempo de espera de connect |
| La solicitud se bloquea y luego falla | httpx.ConnectTimeout / httpx.ReadTimeout | Salida residencial lenta o destino bloqueado | Aumenta el tiempo de espera, vuelve a intentarlo con un nuevo ID de sesión |
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
def get_via_proxy(url: str) -> httpx.Response | None:
try:
with httpx.Client(proxy=PROXY, timeout=httpx.Timeout(15.0, connect=8.0)) as client:
r = client.get(url)
if r.status_code == 407:
raise RuntimeError("proxy rejected credentials (407)")
return r
except httpx.ProxyError as e:
print("proxy refused tunnel:", e)
except httpx.ConnectError as e:
print("cannot connect to proxy host:", e)
except httpx.TimeoutException as e:
print("timed out:", type(e).__name__)
return None
resp = get_via_proxy("https://api.ipify.org?format=json")
print(resp.json() if resp else "failed")
Todas las excepciones de httpx se derivan de httpx.HTTPError, por lo que basta con un único código genérico except para el registro; sin embargo, diferenciar entre ProxyError y ConnectError te indica si debes comprobar las credenciales o la red.
Errores comunes
Uso de https:// en la URL del proxy
Como se ha explicado anteriormente, el esquema de la URL del proxy describe el salto hasta el proxy. Utiliza http:// para los puertos HTTP(S), incluso cuando el destino sea https://. Reserva https:// para las puertas de enlace que anuncian explícitamente TLS al proxy.
Mezclar HTTPTransport con un AsyncClient
mounts en AsyncClient debe utilizar AsyncHTTPTransport. El transporte de sincronización se acepta en el momento de la construcción y falla en el momento de la solicitud con un error poco útil, por lo que es fácil pasar esto por alto en pruebas que nunca envían una solicitud.
Esperar una sola IP del puerto rotativo
El puerto 9000 rota por conexión. Si dos solicitudes consecutivas deben compartir una misma IP, utiliza el puerto 10000 con un identificador -session- en el nombre de usuario. Reutilizar un Client no fija la IP por sí solo, ya que las conexiones del grupo pueden ser sustituidas.
Permitir que el entorno anule tu código
Un HTTPS_PROXY aislado en una imagen de Docker o en un ejecutor de CI redirigirá el tráfico que creías que era directo, y NO_PROXY puede excluir hosts que esperabas que se redirigieran a través del proxy. Establece trust_env=False cuando el enrutamiento del proxy deba ser determinista.
Siguiente paso
Una vez que el cliente anterior devuelve una IP residencial y se mantiene una sesión persistente entre las solicitudes, la variable que queda por determinar es el grupo de servidores que hay detrás. Los proxies residenciales de Geonode exponen los puntos finales utilizados en esta guía: 2,5 millones de direcciones IP en línea, entre 30 y 50 millones de direcciones IP únicas que rotan mensualmente en más de 195 países, con segmentación por país, estado, ciudad y ASN a través de la sintaxis del nombre de usuario y sesiones persistentes de hasta 24 horas. Los precios comienzan en 0,27 $/GB por volumen; el plan básico es de 10 GB por 7,92 $ al mes, y una prueba de 3 días cuesta 5 $. Si estás desarrollando un rastreador basado en esto, el caso práctico de web scraping describe cómo combinar los proxies con la lógica de reintentos, y los proxies de centros de datos rotativos son la opción más económica para objetivos que no bloquean los rangos de direcciones de centros de datos.
Preguntas frecuentes
¿Cómo configuro un proxy httpx para todas las solicitudes?
Pasa la URL del proxy al constructor del cliente: httpx.Client(proxy="http://user:pass@host:port"). Todas las solicitudes realizadas a través de ese cliente, tanto para los destinos http:// como para https://, se redirigen a través del proxy. La misma palabra clave funciona en httpx.AsyncClient y en ayudantes de nivel superior como httpx.get(). Las credenciales de la URL se envían automáticamente como Proxy-Authorization.
¿Cuál es la diferencia entre el proxy y los «mounts» en httpx?
proxy es un atajo que redirige todo a través de un único proxy. mounts asigna patrones de URL, como "http://" o "https://", a transportes, cada uno de los cuales puede tener su propio proxy, reintentos y límites. Utiliza mounts cuando necesites diferentes proxies por esquema o por host, o cuando quieras evitar el proxy para algunos destinos montando un transporte sin proxy.
¿Por qué falla mi montaje https:// cuando la URL del proxy empieza por https://??
El esquema de la URL del proxy describe cómo httpx se comunica con el proxy, no con el destino. Para destinos https://, httpx abre un túnel CONNECT a través de una conexión HTTP simple con el proxy, por lo que la URL del proxy debería utilizar normalmente http://. El uso de https:// hace que httpx negocie TLS con el propio proxy, algo que la mayoría de los puntos finales de proxy no admiten.
¿Admite httpx los proxies SOCKS5?
Sí, pero solo tras instalar la dependencia opcional con pip install httpx[socks], que incluye socksio. A continuación, pasa proxy="socks5://user:pass@host:port" exactamente igual que lo harías con un proxy HTTP. Con Geonode, el puerto rotativo de SOCKS5 es el 11000 y el puerto fijo de SOCKS5 es el 12000; los puertos HTTP(S) no aceptan handshakes SOCKS.
¿Puede httpx reintentar una solicitud automáticamente cuando falla la conexión al proxy?
Solo a nivel de transporte. httpx.HTTPTransport(retries=2) reintenta los intentos de conexión fallidos, incluidos los errores de conexión al proxy, pero nunca reintenta una solicitud que ya haya recibido una respuesta como 407 o 502. Los reintentos en función de los códigos de estado HTTP deben realizarse en tu propio bucle, donde puedes cambiar el identificador de sesión o reducir la frecuencia de forma deliberada.
¿Existen límites a lo que puede hacer una configuración de proxy de httpx con direcciones IP residenciales?
Un proxy cambia tu IP de salida; no cambia lo que permite un sitio web. Respeta el archivo robots.txt, las condiciones de uso y los límites de frecuencia, mantén un nivel moderado de concurrencia y evita recopilar datos personales que no tengas motivos para tratar. Las sesiones residenciales también tienen un límite de 24 horas, tras las cuales se libera la IP y debes abrir una nueva sesión.