Integración7 min de lectura

Proxies en aiohttp 3.14: auth, trust_env y errores

Configura un proxy en aiohttp 3.14: sustituye BasicAuth y proxy_auth, ya obsoletos, activa trust_env y entiende los errores 407 y Bad status line. Probado en 3.14.4.

En esta página

aiohttp 3.14.0 (1 de junio de 2026) declaró obsoletos BasicAuth y el parámetro proxy_auth (registro de cambios, #12499). El sustituto es una cabecera Proxy-Authorization construida con aiohttp.encode_basic_auth():

python
import asyncio

import aiohttp

PROXY = "http://proxy.example.com:8080"
PROXY_HEADERS = {"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}


async def main() -> None:
    async with aiohttp.ClientSession(proxy=PROXY) as session:
        async with session.get("https://example.com/", proxy_headers=PROXY_HEADERS) as response:
            print(response.status)


asyncio.run(main())

En nuestro laboratorio esto autenticó todas las peticiones a https:// sin ningún DeprecationWarning. No autenticó las peticiones a http:// sin cifrar: aiohttp 3.14.4 envió proxy_headers solo con la petición CONNECT que abre un túnel HTTPS, así que para una URL http:// el proxy no recibió credenciales y respondió 407.

Credenciales pasadas comoURL https://URL http://Avisos de obsolescencia
proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(...)}200407ninguno
proxy="http://user:pass@host:port"200200ninguno
proxy_auth=aiohttp.BasicAuth(...)200200dos por llamada

Estos resultados se registraron el 6 de octubre de 2026 con aiohttp 3.14.4 y aiohttp-socks 0.12.0 sobre Python 3.14.4, ejecutando con python -W always contra un proxy local que exige autenticación Basic. aiohttp 3.14.0 y 3.14.3 dieron los mismos resultados.

Migrar desde proxy_auth=aiohttp.BasicAuth

La llamada antigua sigue funcionando en 3.14.4 con los dos esquemas de URL:

python
async with session.get(url, proxy=PROXY, proxy_auth=aiohttp.BasicAuth("user", "pass")) as response:
    print(response.status)

Cada llamada imprimió dos avisos:

code
DeprecationWarning: BasicAuth is deprecated and will be removed in aiohttp 4.0; use aiohttp.encode_basic_auth() with headers={'Authorization': ...} instead
DeprecationWarning: The 'proxy_auth' parameter is deprecated and will be removed in v4; pass proxy_headers={'Proxy-Authorization': aiohttp.encode_basic_auth(login, password)} instead

El sustituto:

python
proxy_headers = {"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}
async with session.get(url, proxy=PROXY, proxy_headers=proxy_headers) as response:
    print(response.status)

Comprueba cuatro cosas antes de desplegarlo:

  • Las URL http:// sin cifrar pierden sus credenciales. La documentación muestra proxy_headers con una URL http://. En el laboratorio esa petición llegó al proxy sin Proxy-Authorization, y sin una cabecera propia que añadimos a proxy_headers, y volvió como 407. La incidencia #4422 informó de lo mismo con aiohttp 3.6.2. Si pides URL http://, pon las credenciales en la URL del proxy: proxy="http://user:pass@host:port" devolvió 200 con los dos esquemas y sin avisos.
  • ClientSession no tiene argumento proxy_headers. La documentación muestra ClientSession(proxy=..., proxy_headers=...). En 3.14.4 lanzó TypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers'. Define proxy= en la sesión y pasa proxy_headers= con cada petición, como en el primer ejemplo.
  • No muevas la cabecera a headers=. Con una URL https://, las cabeceras de la petición viajan dentro del túnel. Con Proxy-Authorization en headers=, el servidor de destino del laboratorio recibió las credenciales del proxy.
  • La codificación por defecto cambió. encode_basic_auth() usa UTF-8 y BasicAuth usaba latin1. Con la contraseña päss los dos produjeron valores de cabecera distintos, así que pasa encoding="latin1" si tu proxy espera los bytes antiguos.

encode_basic_auth() es nueva en 3.14 (referencia). En aiohttp 3.13.5 el mismo código lanzó AttributeError: module aiohttp has no attribute encode_basic_auth.

Las variables de entorno necesitan trust_env=True

Una ClientSession() por defecto ignora las variables de proxy. Con HTTP_PROXY definida, la petición fue directa al destino y el proxy no registró nada. Esta sesión sí usó el proxy:

python
async with aiohttp.ClientSession(trust_env=True) as session:
    async with session.get("http://example.com/") as response:
        print(response.status)

La documentación dice que aiohttp lee las variables mediante urllib.request.getproxies(), las aplica a los esquemas HTTP, HTTPS, WS y WSS, deja que los hosts de no_proxy eviten el proxy y puede tomar las credenciales del proxy de ~/.netrc. En el laboratorio, con trust_env=True:

  • HTTP_PROXY y http_proxy en minúsculas se aplicaron a las URL http://, y HTTPS_PROXY a las URL https://. Con solo HTTP_PROXY definida, una petición a https:// fue directa.
  • ALL_PROXY se ignoró.
  • Las credenciales en la URL de la variable autenticaron los dos esquemas y no imprimieron ningún aviso.
  • NO_PROXY=127.0.0.1 envió la petición directa. No tuvo efecto sobre un argumento proxy= explícito, que además tuvo prioridad sobre la variable.
  • Una variable con una URL de proxy https:// se omitió. aiohttp registró HTTPS proxies https://127.0.0.1:8888 are not supported, ignoring y se conectó directamente.

Variables de entorno de proxy compara estas reglas en otros clientes.

ClientHttpProxyError: 407 con URL https://

Cuando el proxy rechaza la petición CONNECT, aiohttp lanza la excepción antes de que nada llegue al destino. Con una contraseña incorrecta en la URL del proxy:

code
aiohttp.client_exceptions.ClientHttpProxyError: 407, message='Proxy Authentication Required', url='http://user:wrong@127.0.0.1:8888'

La contraseña está en el mensaje, así que va adonde vaya la excepción: logs, trazas, gestores de errores. Con las credenciales en proxy_headers, o tomadas de HTTPS_PROXY, el mismo fallo imprimió url='http://127.0.0.1:8888'.

Mantén las credenciales fuera de la URL del proxy para el tráfico https://. Donde las necesites en la URL, registra exc.status y exc.message (407 y Proxy Authentication Required en el laboratorio) en lugar del texto de la excepción. Corrige el error de proxy 407 da el orden de comprobaciones cuando las credenciales parecen correctas.

Estado 407 sin excepción con URL http://

Con una URL http://, el 407 del proxy es una respuesta normal. No se lanza nada, response.status es 407 y la cabecera Proxy-Authenticate contiene el desafío. El código que solo captura excepciones lo trata como un éxito.

Comprueba response.status, o crea la sesión con raise_for_status=True. Eso lanzó:

code
aiohttp.client_exceptions.ClientResponseError: 407, message='Proxy Authentication Required', url='http://127.0.0.1:8080/'

raise_for_status=True en la petición y response.raise_for_status() lanzaron el mismo error. La clase es ClientResponseError y la URL es la del destino, no la del proxy. ClientHttpProxyError es una subclase suya, así que except aiohttp.ClientResponseError con una comprobación de exc.status == 407 cubre los dos esquemas.

Bad status line: b'\x05\x00' con una URL de proxy socks5://

aiohttp no rechaza proxy="socks5://...". Envía una petición HTTP al puerto SOCKS y falla con la respuesta:

code
aiohttp.client_exceptions.ClientResponseError: 400, message="Bad status line:\n  Expected HTTP/, RTSP/ or ICE/:\n\n  b'\\x05\\x00'\n    ^", url='http://127.0.0.1:8080/'

\x05\x00 es el comienzo de la respuesta del servidor SOCKS5, leído como si fuera una línea de estado HTTP. socks5h:// falló igual. La solución es un conector SOCKS, que se muestra más abajo.

ClientConnectorSSLError con una URL de proxy https://

Una URL de proxy https:// le dice a aiohttp que abra TLS con el propio proxy. Contra el proxy del laboratorio, que habla HTTP sin cifrar, la petición falló durante el handshake:

code
aiohttp.client_exceptions.ClientConnectorSSLError: Cannot connect to host 127.0.0.1:8888 ssl:default [[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1081)]

El proxy vio un ClientHello de TLS y respondió con un 400 en texto plano. El esquema de la URL del proxy describe la conexión con el proxy, no con el destino: una URL de proxy http:// lleva las peticiones a https:// mediante CONNECT. El texto del error viene de OpenSSL 3.5.5 y puede ser distinto en otras versiones.

DeprecationWarning: BasicAuth is deprecated

Basta con construir aiohttp.BasicAuth(...) para provocar el primer aviso, antes de cualquier petición. El parámetro auth= para las credenciales del sitio web también está obsoleto. Su aviso indica headers={'Authorization': aiohttp.encode_basic_auth(login, password)} como sustituto.

Puede que no veas ninguno de los dos. Por defecto, Python muestra un DeprecationWarning solo cuando el código que lo provocó está en __main__. En el laboratorio, la llamada obsoleta imprimió sus avisos cuando estaba en el script que se ejecutaba y no imprimió nada cuando estaba en un módulo importado. python -W always los imprimió en los dos sitios. python -W error convirtió el primero en una excepción, lo que encuentra cada punto de llamada en una ejecución de tests.

Proxies SOCKS: usa aiohttp-socks

sh
python -m pip install aiohttp-socks
python
import aiohttp
from aiohttp_socks import ProxyConnector

connector = ProxyConnector.from_url("socks5://127.0.0.1:1080")
async with aiohttp.ClientSession(connector=connector) as session:
    async with session.get("https://example.com/") as response:
        print(response.status)

Con aiohttp-socks 0.12.0 esto devolvió 200 con URL http:// y https://, y el servidor SOCKS registró cada conexión. Dos detalles:

  • socks5:// ya envía el nombre de host al proxy. Para http://localhost:8080/ el servidor registró un nombre de dominio. Con rdns=False registró 127.0.0.1.
  • ProxyConnector.from_url("socks5h://...") lanzó ValueError: Invalid scheme component: socks5h. SOCKS5 vs SOCKS5h muestra qué hace cada cliente con los dos esquemas.

Requests y HTTPX necesitan sus propios paquetes para SOCKS, que se tratan en Missing dependencies for SOCKS support.

Reintentos tras errores de proxy

Un 407, el error de línea de estado de SOCKS y el error de TLS de arriba son errores de configuración. La misma petición vuelve a fallar igual, así que corrige la configuración en lugar de reintentar. Con timeouts y conexiones cortadas, reintenta solo las peticiones que sea seguro repetir. Un POST puede haber llegado al destino antes de que fallara la conexión con el proxy. Reintentos con proxy: una respuesta perdida, dos trabajos creados muestra ese caso y qué cambia una clave de idempotencia.

Si usas HTTPX para el trabajo asíncrono, consulta Proxies async en HTTPX. Si quieres ayuda de diagnóstico dentro de un agente de programación, Proxy Toolkit MCP acepta descripciones de errores saneadas. Mantén las credenciales fuera de sus argumentos.

Cómo se probó

En Ubuntu 26.04.1 con Python 3.14.4 y OpenSSL 3.5.5, todo en 127.0.0.1: python -m http.server como destinos HTTP y HTTPS, un proxy que exige autenticación Basic y registra cada petición con los nombres de sus cabeceras, un servidor SOCKS5 que registra cada destino y un destino HTTPS que informa de las cabeceras que recibió. Cada uno de los 67 casos se ejecutó en un intérprete nuevo con un entorno limpio. No se usó ninguna cuenta de proxy.

El archivo del laboratorio contiene el ejecutor, los casos, el proxy, el servidor SOCKS5, el README y los resultados registrados.

No se probó: macOS y Windows, autenticación de proxy Digest y NTLM, proxies SOCKS con autenticación, peticiones WebSocket, credenciales de ~/.netrc y proxies a los que se llega por TLS.

ipvolt es un servicio de proxies para desarrolladores que todavía no está abierto; únete a la lista de acceso anticipado y recibirás un solo correo cuando abra.

Fuentes y lecturas adicionales

Referencias técnicas usadas para esta guía. Consulta la documentación de tu versión instalada y la configuración compatible de tu proveedor.