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

Source: https://ipvolt.com/zh/guides/curl-cffi-proxy-setup
Markdown: https://ipvolt.com/zh/guides/curl-cffi-proxy-setup.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / curl_cffi 代理配置：认证、407 错误与 SOCKS

集成
发布于: 2026-10-10
阅读约 8 分钟
作者： ipvolt

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：代理哪一跳出错](/zh/guides/fix-ssl-wrong-version-number-proxy)解释了这条信息。

## CONNECT tunnel failed, response 407：是 ConnectionError，不是 ProxyError

```text
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_cffi | libcurl | `CONNECT` 被拒绝 | SOCKS 失败 |
| --- | --- | --- | --- |
| 0.16.0 到 0.16.3 | 8.21.0 | `ConnectionError`，curl 代码 7 | `ProxyError`，curl 代码 97 |
| 0.7.4 到 0.15.0 | 8.7.0 到 8.15.0 | `ProxyError`，curl 代码 56 | `ProxyError`，curl 代码 97 |

直到 0.15.0，信息都是 `curl: (56) CONNECT tunnel failed, response 407`。curl 命令行在 8.18.0 把退出码从 56 改成了 7。[不靠猜测修复代理错误 407](/zh/guides/fix-proxy-error-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](https://github.com/lexiforest/curl_cffi/issues/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 环境变量](https://curl.se/libcurl/c/libcurl-env.html)）。原因在于 CGI：服务器会把请求头 `Proxy:` 变成脚本的 `HTTP_PROXY` 变量（[Everything curl](https://everything.curl.dev/usingcurl/proxies/env.html)）。

Scrapy 会读取大写的 `HTTP_PROXY`（[Scrapy 2.19 代理配置](/zh/guides/scrapy-proxy-setup)），而 aiohttp 在你传入 `trust_env=True` 之前不读取任何变量（[aiohttp 3.14 代理配置](/zh/guides/aiohttp-proxy-setup)）。[代理环境变量](/zh/guides/proxy-environment-variables)对比了更多客户端。

有两个结果容易被忽略：

- **`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](https://curl.se/libcurl/c/CURLOPT_PROXY.html)）。

## 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 中使用代理](/zh/guides/curl-proxy-setup)）。[socks5 与 socks5h](/zh/blog/socks5-vs-socks5h) 列出了不遵循这个约定的客户端。

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

```text
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，或者发送幂等键。[代理重试：丢失一个响应，创建了两个任务](/zh/blog/proxy-retries-duplicate-jobs)展示了幂等键能改变什么。如果想在编码代理中获得诊断帮助，[Proxy Toolkit MCP](/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 的十二个版本。没有使用任何代理账号。

[实验归档](https://ipvolt.com/downloads/curl-cffi-proxy-setup/curl-cffi-proxy-lab.zip)包含[运行脚本](https://ipvolt.com/downloads/curl-cffi-proxy-setup/run_lab.py)、[用例](https://ipvolt.com/downloads/curl-cffi-proxy-setup/cases.py)、[代理](https://ipvolt.com/downloads/curl-cffi-proxy-setup/auth_proxy.py)、[SOCKS5 服务器](https://ipvolt.com/downloads/curl-cffi-proxy-setup/socks5_server.py)、[README](https://ipvolt.com/downloads/curl-cffi-proxy-setup/README.md)、[记录的结果](https://ipvolt.com/downloads/curl-cffi-proxy-setup/results.json)和[版本对比](https://ipvolt.com/downloads/curl-cffi-proxy-setup/versions.json)。

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

ipvolt 是一项面向开发者的代理服务，目前尚未开放；[加入早期访问名单](https://ipvolt.com/#waitlist-closing)，开放时你会收到一封邮件。

## 参考来源与延伸阅读

- [curl_cffi documentation: Proxies](https://curl-cffi.readthedocs.io/en/latest/advanced.html#proxies)
- [curl_cffi API reference: Session (proxy, proxies, proxy_auth, trust_env, retry)](https://curl-cffi.readthedocs.io/en/latest/api.html#curl_cffi.requests.Session)
- [curl_cffi API reference: AsyncSession](https://curl-cffi.readthedocs.io/en/latest/api.html#curl_cffi.requests.AsyncSession)
- [curl_cffi API reference: Exceptions and warnings (ProxyError, ConnectionError)](https://curl-cffi.readthedocs.io/en/latest/api.html#exceptions-and-warnings)
- [curl_cffi documentation: Using CURLOPT_* in requests API (curl_options)](https://curl-cffi.readthedocs.io/en/latest/advanced.html#using-curlopt-in-requests-api)
- [curl_cffi issue #326: CONNECT was sent for http:// URLs, changed in 0.7.0b7](https://github.com/lexiforest/curl_cffi/issues/326)
- [libcurl: CURLOPT_PROXY (schemes, the empty string, socks5h)](https://curl.se/libcurl/c/CURLOPT_PROXY.html)
- [libcurl: CURLOPT_NOPROXY and the no_proxy variable](https://curl.se/libcurl/c/CURLOPT_NOPROXY.html)
- [libcurl environment variables: http_proxy is only used lowercase](https://curl.se/libcurl/c/libcurl-env.html)
- [Everything curl: proxy environment variables and the CGI reason](https://everything.curl.dev/usingcurl/proxies/env.html)
- [libcurl error codes: CURLE_COULDNT_CONNECT (7), CURLE_RECV_ERROR (56), CURLE_PROXY (97)](https://curl.se/libcurl/c/libcurl-errors.html)

## 相关指南

- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md)
- [在 curl 中使用代理：-x、环境变量、SOCKS5 与认证](https://ipvolt.com/zh/guides/curl-proxy-setup.md)
- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md)

## 关于 ipvolt

示例使用通用的代理设置，并附上原始技术文档链接。具体产品的行为请向你的服务商确认。ipvolt 仍在开发中。

[阅读英文原文](https://ipvolt.com/guides/curl-cffi-proxy-setup.md)

## 了解何时开放体验。

ipvolt · 开发中

我们正在为开发者和数据团队打造代理基础设施。加入意向名单，ipvolt 就绪时第一时间收到通知。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/guides/curl-cffi-proxy-setup#waitlist-closing)

[隐私政策](https://ipvolt.com/privacy)

