集成阅读约 7 分钟

aiohttp 3.14 代理配置:认证、trust_env 与报错

在 aiohttp 3.14 中配置代理:替换已弃用的 BasicAuth 和 proxy_auth,启用 trust_env,并看懂 407 与 Bad status line 报错。基于 3.14.4 实测。

本页内容

aiohttp 3.14.0(2026 年 6 月 1 日)弃用了 BasicAuth 和 proxy_auth 参数(更新日志,#12499)。替代做法是用 aiohttp.encode_basic_auth() 生成 Proxy-Authorization 请求头:

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())

在我们的实验环境里,这种写法让每个 https:// 请求都通过了认证,并且没有 DeprecationWarning。它没有让普通的 http:// 请求通过认证:aiohttp 3.14.4 只在建立 HTTPS 隧道的 CONNECT 请求里发送 proxy_headers,所以对 http:// 的 URL,代理没有收到凭据,返回了 407。

凭据的传递方式https:// URLhttp:// URL弃用警告
proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(...)}200407无
proxy="http://user:pass@host:port"200200无
proxy_auth=aiohttp.BasicAuth(...)200200每次调用两条

这些结果记录于 2026 年 10 月 6 日,使用 aiohttp 3.14.4 和 aiohttp-socks 0.12.0,运行在 Python 3.14.4 上,以 python -W always 启动,对着一个要求 Basic 认证的本地代理。aiohttp 3.14.0 和 3.14.3 的结果相同。

从 proxy_auth=aiohttp.BasicAuth 迁移

旧的调用在 3.14.4 中对两种 URL 协议仍然有效:

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

每次调用打印两条警告:

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

替代写法:

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)

上线前检查四件事:

  • 普通 http:// URL 会丢掉凭据。 文档里的示例把 proxy_headers 和 http:// 的 URL 一起使用。在实验中,这个请求到达代理时没有 Proxy-Authorization,也没有我们加在 proxy_headers 里的自定义请求头,返回的是 407。Issue #4422 针对 aiohttp 3.6.2 报告过同样的现象。如果你要请求 http:// 的 URL,把凭据写进代理 URL:proxy="http://user:pass@host:port" 对两种协议都返回 200,且没有警告。
  • ClientSession 没有 proxy_headers 参数。 文档里有 ClientSession(proxy=..., proxy_headers=...) 的写法。在 3.14.4 中它抛出了 TypeError: ClientSession.__init__() got an unexpected keyword argument 'proxy_headers'。在会话上设置 proxy=,每次请求再传 proxy_headers=,就像第一个示例那样。
  • 不要把这个请求头挪到 headers=。 对 https:// 的 URL,请求头在隧道内部传输。把 Proxy-Authorization 放进 headers= 后,实验中的目标服务器收到了代理凭据。
  • 默认编码变了。 encode_basic_auth() 使用 UTF-8,而 BasicAuth 使用 latin1。对密码 päss,两者生成的请求头值不同,所以如果你的代理需要旧的字节,请传入 encoding="latin1"。

encode_basic_auth() 是 3.14 新增的(参考文档)。在 aiohttp 3.13.5 上,同样的代码抛出了 AttributeError: module aiohttp has no attribute encode_basic_auth。

环境变量需要 trust_env=True

默认的 ClientSession() 会忽略代理环境变量。设置了 HTTP_PROXY 时,请求直接发往目标,代理什么也没有记录。下面这个会话使用了代理:

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

文档说明,aiohttp 通过 urllib.request.getproxies() 读取这些变量,把它们用于 HTTP、HTTPS、WS 和 WSS 协议,让 no_proxy 中的主机绕过代理,并且可以从 ~/.netrc 读取代理凭据。在实验中,使用 trust_env=True 时:

  • HTTP_PROXY 和小写的 http_proxy 对 http:// 的 URL 生效,HTTPS_PROXY 对 https:// 的 URL 生效。只设置 HTTP_PROXY 时,https:// 请求是直连的。
  • ALL_PROXY 被忽略。
  • 变量的 URL 里的凭据让两种协议都通过了认证,并且没有打印警告。
  • NO_PROXY=127.0.0.1 让请求直连。它对显式的 proxy= 参数不起作用,而且显式参数的优先级高于变量。
  • 值为 https:// 代理 URL 的变量被跳过。aiohttp 记录了 HTTPS proxies https://127.0.0.1:8888 are not supported, ignoring,然后直接连接。

代理环境变量比较了这些规则在其他客户端中的情况。

https:// URL 上的 ClientHttpProxyError: 407

当代理拒绝 CONNECT 请求时,aiohttp 在任何内容到达目标之前就抛出异常。代理 URL 里密码错误时:

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

密码就在消息里,所以异常去哪里,它就去哪里:日志、调用栈、错误追踪系统。当凭据放在 proxy_headers 里,或者取自 HTTPS_PROXY 时,同样的失败打印的是 url='http://127.0.0.1:8888'。

对 https:// 流量,不要把凭据放在代理 URL 里。在必须写进 URL 的地方,记录 exc.status 和 exc.message(实验中是 407 和 Proxy Authentication Required),而不是异常文本。当凭据看起来没错时,修复代理错误 407 给出了检查顺序。

http:// URL 上状态码 407 但没有异常

对 http:// 的 URL,代理的 407 是一个普通响应。不会抛出异常,response.status 是 407,Proxy-Authenticate 响应头里是认证质询。只捕获异常的代码会把它当成成功。

检查 response.status,或者用 raise_for_status=True 创建会话。这样抛出了:

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

在请求上使用 raise_for_status=True 和调用 response.raise_for_status() 抛出的是同一个错误。这里的类是 ClientResponseError,URL 是目标的 URL,不是代理的。ClientHttpProxyError 是它的子类,所以 except aiohttp.ClientResponseError 加上对 exc.status == 407 的判断可以覆盖两种协议。

socks5:// 代理 URL 导致的 Bad status line: b'\x05\x00'

aiohttp 不会拒绝 proxy="socks5://..."。它向 SOCKS 端口发送一个 HTTP 请求,然后在读取响应时失败:

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 是 SOCKS5 服务器响应的开头,被当成了 HTTP 状态行来读取。socks5h:// 以同样的方式失败。解决办法是使用 SOCKS 连接器,见下文。

https:// 代理 URL 导致的 ClientConnectorSSLError

https:// 的代理 URL 让 aiohttp 与代理本身建立 TLS。对着实验中只说明文 HTTP 的代理,请求在握手阶段失败:

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)]

代理看到的是 TLS ClientHello,并用明文的 400 作了回应。代理 URL 的协议描述的是到代理的连接,而不是到目标的连接:http:// 的代理 URL 通过 CONNECT 承载 https:// 请求。这段错误文本来自 OpenSSL 3.5.5,在其他版本上可能不同。

DeprecationWarning: BasicAuth is deprecated

只要构造 aiohttp.BasicAuth(...) 就会触发第一条警告,此时还没有发出任何请求。用于网站凭据的 auth= 参数也被弃用了。它的警告给出的替代写法是 headers={'Authorization': aiohttp.encode_basic_auth(login, password)}。

你可能两条都看不到。默认情况下,只有当触发它的代码位于 __main__ 时,Python 才显示 DeprecationWarning。在实验中,被弃用的调用位于被运行的脚本里时打印了警告,位于被导入的模块里时什么也没打印。python -W always 在两处都打印了警告。python -W error 把第一条变成了异常,这样跑一遍测试就能找出每个调用点。

SOCKS 代理:使用 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)

使用 aiohttp-socks 0.12.0 时,这段代码对 http:// 和 https:// 的 URL 都返回 200,SOCKS 服务器记录了每个连接。两个细节:

  • socks5:// 已经把主机名发给代理。对 http://localhost:8080/,服务器记录的是域名。使用 rdns=False 时,它记录的是 127.0.0.1。
  • ProxyConnector.from_url("socks5h://...") 抛出了 ValueError: Invalid scheme component: socks5h。SOCKS5 与 SOCKS5h 的区别展示了各个客户端如何处理这两种协议。

Requests 和 HTTPX 需要各自的 SOCKS 依赖包,详见解决 Missing dependencies for SOCKS support。

代理出错后的重试

407、SOCKS 的状态行错误和上面的 TLS 错误都是配置错误。同一个请求会以同样的方式再次失败,所以应该修正配置,而不是重试。对超时和断开的连接,只重试可以安全重复的请求。POST 可能在代理连接失败之前就已经到达目标。代理重试:丢失一个响应,创建了两个任务展示了这种情况,以及幂等键能改变什么。

如果你用 HTTPX 做异步请求,请看 HTTPX 异步代理。如果想在编码代理中获得诊断帮助,Proxy Toolkit MCP 接受经过清理的错误描述。不要把凭据放进它的参数里。

测试方法

在 Ubuntu 26.04.1 上,使用 Python 3.14.4 和 OpenSSL 3.5.5,一切都在 127.0.0.1 上:python -m http.server 作为 HTTP 和 HTTPS 目标,一个要求 Basic 认证并记录每个请求及其请求头名称的代理,一个记录每个目的地的 SOCKS5 服务器,以及一个报告自己收到哪些请求头的 HTTPS 目标。67 个用例中的每一个都在新的解释器和干净的环境中运行。没有使用任何代理账号。

实验归档包含运行脚本、用例、代理、SOCKS5 服务器、README 和记录的结果。

未测试:macOS 和 Windows、Digest 和 NTLM 代理认证、需要认证的 SOCKS 代理、WebSocket 请求、~/.netrc 中的凭据,以及需要通过 TLS 连接的代理。

ipvolt 是一项面向开发者的代理服务,目前尚未开放;加入早期访问名单,开放时你会收到一封邮件。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。