集成阅读约 8 分钟

curl_cffi 代理配置:认证、407 错误与 SOCKS

curl_cffi 0.16.3 代理配置:凭据与 impersonate,CONNECT 遇 407 抛出 ConnectionError 而非 ProxyError,HTTP_PROXY 规则与 socks5h。

本页内容

把凭据写进代理 URL,然后通过 proxy 参数传入:

python
from curl_cffi import requests

PROXY = "http://user:pass@proxy.example.com:8080"

with requests.Session(impersonate="chrome", proxy=PROXY) as session:
    response = session.get("https://example.com/")
    print(response.status_code)

要捕获代理故障,需要同时捕获 ProxyError 和 ConnectionError,并检查 status_code 是否为 407。在 curl_cffi 0.16 中,代理拒绝 CONNECT 时抛出的是 ConnectionError,不是 ProxyError。

在我们的实验环境里,这段代码对 https:// 和 http:// 请求都通过了一个要求 Basic 认证的本地代理。下文的所有内容记录于 2026 年 10 月 10 日,环境是 Python 3.14.4 上的 curl_cffi 0.16.3(libcurl 8.21.0)。impersonate 用来选择浏览器配置,curl_cffi 会照它设置 TLS 和 HTTP/2 参数。下面的每个结果在使用和不使用它时都相同。

讲错误之前先说三个细节:

  • proxy 和 proxies 只能用一个。 proxies={"http": PROXY, "https": PROXY} 在单个请求和 Session 上效果相同。字典的键是所请求 URL 的协议:只有 "https" 键时,http:// 请求走了直连。同时传入两个参数会抛出 TypeError: Cannot specify both 'proxy' and 'proxies'。
  • URL 里的密码要做百分号编码。 p@ss:w/rd 写成 p%40ss%3Aw%2Frd 可以正常使用。不编码时,请求抛出 ProxyError,信息为 curl: (5) Unsupported proxy syntax。代理 URL 不带凭据、另外传 proxy_auth=("user", "p@ss:w/rd") 也能用,而且不需要编码。
  • HTTP 代理要写 http://,请求 https:// URL 时也一样。 这个协议描述的是你和代理之间的连接。把 https:// 代理 URL 指向实验环境里的普通 HTTP 代理时,抛出了带 curl: (35) TLS connect error 和 WRONG_VERSION_NUMBER 的 SSLError。SSL WRONG_VERSION_NUMBER:代理哪一跳出错解释了这条信息。

CONNECT tunnel failed, response 407:是 ConnectionError,不是 ProxyError

code
curl_cffi.requests.exceptions.ConnectionError: Failed to perform, curl: (7) CONNECT tunnel failed, response 407. See https://curl.se/libcurl/c/libcurl-errors.html first for more details.

对于 https:// URL,curl_cffi 会先向代理发送 CONNECT。实验代理返回了 407,请求抛出了上面的错误。不带凭据和密码错误时,这条信息完全相同。

  • isinstance(error, ProxyError) 为 False。ProxyError 和 ConnectionError 是 RequestException 下的两个平级子类,所以 except ProxyError 捕获不到它。
  • error.code 是 7。
  • error.response 是一个 Response,它的 status_code 是 0,不是 407。状态码只出现在错误信息里。
  • 代码 7 并不专指 407。当实验代理连不上目标时,它返回 502,信息是 curl: (7) CONNECT tunnel failed, response 502。代理端口无人监听时,信息是 curl: (7) Failed to connect to 127.0.0.1:8443 over proxy 127.0.0.1 after 0 ms: Could not connect to server。

异常类取决于 curl 的错误代码,而这个代码在不同版本之间变过。我们在十二个版本上运行了同一个请求:

curl_cffilibcurlCONNECT 被拒绝SOCKS 失败
0.16.0 到 0.16.38.21.0ConnectionError,curl 代码 7ProxyError,curl 代码 97
0.7.4 到 0.15.08.7.0 到 8.15.0ProxyError,curl 代码 56ProxyError,curl 代码 97

直到 0.15.0,信息都是 curl: (56) CONNECT tunnel failed, response 407。curl 命令行在 8.18.0 把退出码从 56 改成了 7。不靠猜测修复代理错误 407 里有那张表,以及检查凭据的顺序。

下面这段代码能处理变化前后两种版本里被拒绝的 CONNECT,以及 SOCKS 失败和 407 响应:

python
import re

from curl_cffi.requests.exceptions import ConnectionError, ProxyError

CONNECT_STATUS = re.compile(r"CONNECT tunnel failed, response (\d{3})")


class ProxyFailure(Exception):
    pass


def fetch(session, url):
    try:
        response = session.get(url)
    except ProxyError as error:
        raise ProxyFailure(f"proxy error, curl {error.code}") from error
    except ConnectionError as error:
        rejected = CONNECT_STATUS.search(str(error))
        if error.code == 7 and rejected:
            raise ProxyFailure(f"proxy answered CONNECT with {rejected[1]}") from error
        raise
    if response.status_code == 407:
        raise ProxyFailure("proxy answered 407")
    return response

在实验里,它分别报告了 proxy answered CONNECT with 407、proxy answered CONNECT with 502、针对 http:// URL 的 proxy answered 407,以及针对 SOCKS 失败的 proxy error, curl 97。代理端口关闭时,它把 ConnectionError 原样重新抛出。

http:// URL 的 407:是响应,不是异常

对于 http:// URL,没有隧道。curl_cffi 用普通的 GET 把完整 URL 发给代理,从 0.7.0b7 起就是这样(issue #326),代理的 407 会作为响应返回。

没有抛出任何异常。response.status_code 是 407,response.ok 是 False,响应头里有 Proxy-Authenticate: Basic realm="lab"。只捕获异常的代码会把它当成一次成功但响应体为空的请求。

response.raise_for_status() 抛出了 curl_cffi.requests.exceptions.HTTPError: HTTP Error 407: Proxy Authentication Required。这个异常的 error.response.status_code 是 407。

环境变量:读取 http_proxy,不读取 HTTP_PROXY

代码里没有设置代理时,libcurl 会从环境变量里取:

  • 对于 http:// URL,大写的 HTTP_PROXY 被忽略,请求走了直连。小写的 http_proxy 让请求走了代理。
  • 对于 https:// URL,HTTPS_PROXY 和 https_proxy 都生效。只设置 http_proxy 时,https:// 请求走了直连。
  • ALL_PROXY 和 all_proxy 对两种协议都生效。
  • NO_PROXY=127.0.0.1 和小写的 no_proxy 让请求走了直连。

libcurl 的文档写明了这个例外:这些变量的大写形式也会被检查,"except the http_proxy one which is only used lowercase"(libcurl 环境变量)。原因在于 CGI:服务器会把请求头 Proxy: 变成脚本的 HTTP_PROXY 变量(Everything curl)。

Scrapy 会读取大写的 HTTP_PROXY(Scrapy 2.19 代理配置),而 aiohttp 在你传入 trust_env=True 之前不读取任何变量(aiohttp 3.14 代理配置)。代理环境变量对比了更多客户端。

有两个结果容易被忽略:

  • NO_PROXY 会覆盖 proxy 参数。 设置了 NO_PROXY=127.0.0.1 时,requests.get(url, proxy=PROXY) 在两种协议下都走了直连。加上 curl_options={CurlOpt.NOPROXY: ""} 后,请求重新走了代理。和 http_proxy 冲突时,以参数为准。
  • trust_env=False 没有关掉这些变量。 Session(trust_env=False) 仍然使用了 http_proxy 和 HTTPS_PROXY。proxy="" 也没有关掉它们。

在设置了这些变量的情况下,有两种写法让请求走了直连:

python
from curl_cffi import CurlOpt, requests

requests.get("https://example.com/", proxies={"all": ""})
requests.get("https://example.com/", curl_options={CurlOpt.NOPROXY: "*"})

两者也都可以作为 Session 的参数。空的代理字符串 "explicitly disables the use of a proxy, even if there is an environment variable set for it"(CURLOPT_PROXY)。

SOCKS:socks5:// 在本地解析,socks5h:// 发送主机名

实验通过一个记录每个目的地的 SOCKS5 服务器请求 http://dnsprobe.example:8080/,这个名字只在 hosts 文件里存在,对应 127.0.0.2:

  • 使用 socks5://127.0.0.1:1080 时,服务器记录了 CONNECT atyp=IPv4 dest=127.0.0.2:8080。curl_cffi 自己解析了这个名字。
  • 使用 socks5h://127.0.0.1:1080 时,服务器记录了 CONNECT atyp=DOMAIN dest=dnsprobe.example:8080。代理收到的是主机名。

https:// URL 得到同样的两个结果。这是 curl 自身的约定(在 curl 中使用代理)。socks5 与 socks5h 列出了不遵循这个约定的客户端。

SOCKS 握手失败才是真正抛出 ProxyError 的情况:

code
curl_cffi.requests.exceptions.ProxyError: Failed to perform, curl: (97) cannot complete SOCKS5 connection to 127.0.0.1. (1). See https://curl.se/libcurl/c/libcurl-errors.html first for more details.

error.code 是 97。括号里的最后一个数字是 SOCKS 服务器返回的应答码:实验服务器返回 1,即一般性失败。目标端口关闭、服务器返回 5 时,信息以 (5) 结尾。

协议和端口不匹配时,失败方式又不一样:

  • 把 socks5:// 指向 HTTP 代理端口,抛出带 curl: (97) Received invalid version in initial SOCKS5 response. 的 ProxyError。
  • 把 http:// 指向 SOCKS 端口,请求 https:// URL 时抛出带 curl: (56) Proxy CONNECT aborted 的 ProxyError。
  • SOCKS 端口无人监听时,错误又是代码为 7 的 ConnectionError。

AsyncSession:同样的配置,同样的错误

python
import asyncio

from curl_cffi import AsyncSession

PROXY = "http://user:pass@proxy.example.com:8080"


async def main():
    async with AsyncSession(impersonate="chrome", proxy=PROXY) as session:
        response = await session.get("https://example.com/")
        print(response.status_code)


asyncio.run(main())

每个异步用例都和对应的同步用例一致。被拒绝的 CONNECT 抛出同样的代码为 7 的 ConnectionError。http:// URL 把 407 作为响应返回。SOCKS 失败抛出代码为 97 的 ProxyError。socks5:// 和 socks5h:// 发送的地址类型相同,HTTP_PROXY 被忽略,而 http_proxy 生效。

重试:retry 会重发 POST 请求

curl_cffi 默认不重试。实验通过代理向一个目标发送 POST,这个目标读完请求后不作应答就关闭连接。调用抛出了带 curl: (52) Empty reply from server 的 ConnectionError,目标收到了一次 POST。

使用 requests.Session(proxy=PROXY, retry=1) 时,同样的调用返回了 201,而目标收到了两次请求体。retry 重发 POST 的方式和重发 GET 一样。

CONNECT 被拒绝时,retry=2 发送了三次 CONNECT 请求并抛出同样的错误,Session 和 AsyncSession 都是如此。重试修复不了错误的凭据。

对于不能安全重发的请求,把 retry 保持为 0,或者发送幂等键。代理重试:丢失一个响应,创建了两个任务展示了幂等键能改变什么。如果想在编码代理中获得诊断帮助,Proxy Toolkit MCP 接受经过清理的错误描述。不要把凭据放进它的参数里。

测试方法

在 Ubuntu 26.04.1 上,使用 Python 3.14.4 和 curl_cffi 0.16.3(libcurl 8.21.0-IMPERSONATE,BoringSSL),一切都在回环地址上:python -m http.server 作为 HTTP 和 HTTPS 目标,三个要求 Basic 认证并记录每个请求的代理,一个记录每个目的地的 SOCKS5 服务器和一个对每个请求都返回应答码 1 的 SOCKS5 服务器,以及一个会丢掉第一次 POST 响应的目标。dnsprobe.example 只存在于 hosts 文件的一份私有副本里。86 个用例中的每一个都在新的解释器和干净的环境中运行。版本对比安装了从 0.7.4 到 0.16.3 的十二个版本。没有使用任何代理账号。

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

未测试:macOS 和 Windows、Digest 和 NTLM 代理认证、需要认证的 SOCKS 代理、需要通过 TLS 连接的代理、HTTP/3 和 WebSocket。

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

参考来源与延伸阅读

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