# SSL WRONG_VERSION_NUMBER：代理哪一跳出错

Source: https://ipvolt.com/zh/guides/fix-ssl-wrong-version-number-proxy
Markdown: https://ipvolt.com/zh/guides/fix-ssl-wrong-version-number-proxy.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / SSL WRONG_VERSION_NUMBER：代理哪一跳出错

故障排查
发布于: 2026-10-04
阅读约 8 分钟
作者： ipvolt

使用代理时，同一个 SSL wrong version number 错误可能出在两跳：代理端口或目标端口。教你在 curl、Requests 和 HTTPX 中判断是哪一跳并修复。

`wrong version number` 是 OpenSSL 在发起 TLS 握手后、发现响应的前几个字节不是 TLS 时报告的错误。实际上那些字节是明文 HTTP：客户端向一个以明文应答的端口发送了 ClientHello，而 `HTTP/1.1 400 Bad Request` 这样的响应无法被解析为 TLS 记录。这与证书、协议版本或加密套件都无关，所以 `--tlsv1.2`、`--insecure` 和 `verify=False` 都不会带来任何改变。

使用代理时，这次握手可能发生在两个位置，而大多数客户端对两者打印的文字完全相同：

1. **代理这一跳。** 代理 URL 写的是 `https://`，于是客户端与代理本身开始 TLS，但代理端口说的是明文 HTTP。
2. **目标这一跳。** 代理 URL 写的是 `http://`，`CONNECT` 成功，客户端通过隧道与一个不支持 TLS 的目标端口开始 TLS，例如 `https://example.com:80/`。

| 客户端 | 情况 1：代理这一跳 | 情况 2：目标这一跳 |
| --- | --- | --- |
| curl 8.18.0 | `curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number` | 同一行 |
| Requests 2.34.2 | `ProxyError`：`Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP.` | `SSLError`：`[SSL: WRONG_VERSION_NUMBER] wrong version number`，没有提示 |
| HTTPX 0.28.1 | `ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number` | 同一行 |

只有 Requests 会指明是哪一跳。使用 curl 和 HTTPX 时还需要多做一步检查，下文会说明。Python 两行来自搭配 OpenSSL 3.0.13 的 Python 3.12.3；较新的 OpenSSL 构建对同一故障的措辞不同，本页最后一张表列出了记录到的全部字符串。

## 如何区分这两跳

**先看代理 URL。** 情况 1 要求代理 URL 以 `https://` 开头。打印进程实际使用的值，包括不是你自己设置的变量：

```sh
env | grep -i _proxy
```

如果所有代理 URL 都已经以 `http://` 开头，那就是情况 2。

**curl：读取 CONNECT 状态。** `%{http_connect}` 是代理对 `CONNECT` 的响应状态码。为零表示 curl 根本没有走到那一步。

```sh
curl --silent --show-error --output /dev/null \
  --proxy "$PROXY_URL" \
  --write-out 'connect=%{http_connect} exit=%{exitcode}\n' \
  https://example.com/
```

- `connect=000 exit=35`：与代理的握手失败。情况 1。
- `connect=200 exit=35`：隧道已打开，与目标的握手失败。情况 2。

`curl -v` 显示的是同一件事。情况 2 中，这几行出现在错误之前；情况 1 中完全没有 `CONNECT` 行：

```text
> CONNECT 127.0.0.1:18000 HTTP/1.1
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
* TLS connect error: error:0A00010B:SSL routines::wrong version number
```

详细输出还会打印 `Proxy-Authorization` 请求头，所以分享时请发 write-out 那一行，而不是完整跟踪。

**Requests：看异常类。** `requests.exceptions.ProxyError` 表示到代理的连接失败。`requests.exceptions.SSLError` 表示隧道打开之后 TLS 失败。

```python
import requests

proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
proxies = {"http": proxy_url, "https": proxy_url}
try:
    requests.get("https://example.com/", proxies=proxies, timeout=(10, 20))
except requests.exceptions.ProxyError as error:
    print("proxy hop:", type(error).__name__)
except requests.exceptions.SSLError as error:
    print("target hop:", type(error).__name__)
```

这条提示来自 urllib3，它只在代理 URL 的协议是 `https` 且 TLS 错误包含 `wrong version number`、`unknown protocol` 或 `record layer failure` 时才添加。旧版本没有这么严谨：urllib3 1.26.8 对 `http://` 代理 URL 也会打印这条提示（[issue #2564](https://github.com/urllib3/urllib3/issues/2564)）。在旧版 urllib3 上，请相信你打印出来的代理 URL，而不是提示。

**HTTPX：更换协议，或查看 httpcore 日志。** HTTPX 对两种情况抛出相同的 `ConnectError`。如果代理 URL 是 `https://`，把它改成 `http://` 再运行一次：错误消失说明是情况 1，同样的错误仍在则是情况 2。想直接得到答案，可以打开调试日志：

```python
import logging

logging.basicConfig(level=logging.DEBUG)
```

情况 2 会记录一次返回 200 的 `CONNECT`，随后是 `httpcore.proxy` 中的失败：

```text
DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'Connection established', [])
DEBUG:httpcore.proxy:start_tls.failed exception=ConnectError(SSLError(1, '[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1081)'))
```

情况 1 不会记录 `CONNECT`，失败的那一行是 `httpcore.connection:start_tls.failed`。这段日志来自搭配 OpenSSL 3.5.5 的 Python 3.14.4，所以显示的是 `RECORD_LAYER_FAILURE`。

## 修复情况 1：代理 URL 写 http://

代理 URL 中的协议描述的是客户端如何与代理通信，而不是它通过代理获取什么。`http://` 代理 URL 照样可以承载 HTTPS 流量：客户端以明文发送 `CONNECT`，然后在隧道内与目标进行 TLS。只有当服务商在文档中明确提供 TLS 代理端口时才使用 `https://`。curl 的 `--proxy` 文档把不带协议或 `http://` 视为 HTTP 代理，把 `https://` 视为 HTTPS 代理。

```sh
curl --proxy http://USERNAME:PASSWORD@proxy.example.net:8080 https://example.com/
```

```python
import requests

proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
response = requests.get(
    "https://example.com/",
    proxies={"http": proxy_url, "https": proxy_url},
    timeout=(10, 20),
)
```

```python
import httpx

with httpx.Client(proxy="http://USERNAME:PASSWORD@proxy.example.net:8080") as client:
    response = client.get("https://example.com/")
```

```sh
export HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
export HTTP_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
```

在实验中，用 `https://` 失败的那个代理端口，在协议改为 `http://` 之后（无论写在代码里还是通过 `HTTPS_PROXY` 设置），三个客户端都返回了 200。如果改完之后错误依旧，可能是某个环境变量仍在提供旧值：Requests 和 HTTPX 默认都会读取 `HTTPS_PROXY`（`trust_env`）。[代理环境变量](/zh/guides/proxy-environment-variables)说明了各客户端读取哪个变量，[curl](/zh/guides/curl-proxy-setup)、[Requests](/zh/guides/python-requests-proxy) 和 [HTTPX](/zh/guides/httpx-async-proxy) 指南则给出了完整配置。

## 陷阱：字典的键不是代理的协议

在 `proxies={"https": ...}` 中，键是你所请求 URL 的协议。值是代理 URL，它有自己的协议。两者不必一致，而对于明文 HTTP 代理端口，两者一定不能一致：

```python
# Case 1: TLS to a proxy port that speaks plain HTTP
proxies = {"https": "https://USERNAME:PASSWORD@proxy.example.net:8080"}

# Correct: HTTPS targets through an HTTP proxy
proxies = {"https": "http://USERNAME:PASSWORD@proxy.example.net:8080"}
```

环境变量也是同样的道理。`HTTPS_PROXY` 这个名字决定哪些请求走代理，它的值说明如何连接代理。

## 修复情况 2：改正目标 URL

代理没有问题。是 URL 在一个提供明文 HTTP 的端口上要求 TLS。常见原因是 `https://` 搭配 80 端口或某个内部 HTTP 端口，以及基础 URL 由分开配置的协议和端口拼接而成。通过同一条隧道发送明文 HTTP 来确认：

```sh
curl --silent --show-error --proxy "$PROXY_URL" --proxytunnel \
  --write-out 'connect=%{http_connect} status=%{http_code}\n' \
  http://example.com:80/
```

在实验中，对于用 `https://` 失败的那个端口，这条命令打印出了页面和 `connect=200 status=200`，而真正支持 TLS 的端口给出的是 `curl: (52) Empty reply from server`。这里出现 HTTP 状态码就说明该端口说的是明文 HTTP：把 URL 改成 `http://`，或者指向 TLS 端口。如果允许你不经代理直接连接，同一个 URL 直连时也会以同样的方式失败，这也排除了代理的嫌疑。

如果 URL 没错而错误时有时无，说明从隧道返回的字节并非来自目标的 TLS 服务器。urllib3 的 issue #2564 报告的正是不稳定代理下的这种现象。请记录带时间戳的 `connect=` 行并发给服务商。

## 重试解决不了这两种情况

两种情况都是配置错误，每次尝试都会以同样的方式失败。Requests 消息里的 `Max retries exceeded` 并不代表真的重试过：默认适配器配置为零次重试，却照样打印这段文字。重试策略只会推迟同一个异常。先改正 URL，把重试留给那些在两次尝试之间可能发生变化的故障。

在兼容 MCP 的编程智能体中，免费的 [Proxy Toolkit MCP](/mcp) 可以按请求阶段诊断代理错误。把客户端、异常和 `connect=` 的值告诉它，不要附带凭据。

## 错误文字取决于 OpenSSL 构建

错误文字来自 TLS 库，而不是客户端，所以同一故障在不同构建上的表述不同。除另有说明外，每个构建在两跳上打印的字符串相同：

| 构建 | 错误文字 |
| --- | --- |
| curl 8.18.0 (OpenSSL 3.5.5)、curl 8.17.0 (OpenSSL 3.6.0)、curl 8.22.0 (OpenSSL 4.0.2) | `curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number` |
| curl 8.12.1 (OpenSSL 3.4.1) | `curl: (35) TLS connect error: error:0A0000C6:SSL routines::packet length too long` |
| curl 8.7.1 (OpenSSL 3.2.1) | `curl: (35) OpenSSL/3.2.1: error:0A0000C6:SSL routines::packet length too long` |
| Python 3.11.3 (OpenSSL 1.1.1s)、Python 3.12.3 (OpenSSL 3.0.13) | `[SSL: WRONG_VERSION_NUMBER] wrong version number` |
| Python 3.14.4 (OpenSSL 3.5.5) | `[SSL: RECORD_LAYER_FAILURE] record layer failure` |
| Node.js 24.20.0 `fetch` (OpenSSL 3.5.7) | `TypeError: fetch failed`，cause 的 code 为 `ERR_SSL_WRONG_VERSION_NUMBER` |
| Playwright 1.63.0 (Chromium 153) | 情况 1：`net::ERR_PROXY_CONNECTION_FAILED`。情况 2：`net::ERR_SSL_PROTOCOL_ERROR` |

也就是说，链路中有代理时出现的 `packet length too long` 和 `record layer failure` 同样是这两种情况。在三个 Python 构建上，Requests 在情况 1 中都抛出带提示的 `ProxyError`，在情况 2 中都抛出不带提示的 `SSLError`。urllib3 查找的第三个短语 `unknown protocol` 在所有测试过的构建上都没有出现。Chromium 是这里唯一能自行区分两跳的客户端；[修复 ERR_TUNNEL_CONNECTION_FAILED 错误](/zh/guides/fix-err-tunnel-connection-failed)介绍了它的代理错误，[Node.js fetch failed](/zh/guides/fix-node-fetch-failed-proxy) 指南介绍了如何读取 `cause` 链。

## 测试方法

2026 年 10 月 4 日，在一台 Ubuntu 26.04 VPS 上，所有服务都运行在 `127.0.0.1`：一个对任何输入都以明文 `HTTP/1.1 400 Bad Request` 应答的监听器、一个最小化的明文 HTTP `CONNECT` 代理、作为明文 HTTP 目标的 `python -m http.server`，以及套上一次性证书、作为可用 TLS 目标的同一个处理程序。无需代理账号，也没有任何出站流量。情况 1 把 `https://` 代理 URL 分别指向明文监听器和 `CONNECT` 代理。情况 2 使用 `CONNECT` 代理，并用 `https://` URL 访问明文 HTTP 端口。使用 `http://` 代理 URL 和 TLS 目标的对照组在所有客户端中都返回 200。

客户端：上表中的各个 curl 构建；三个 Python 构建上的 Requests 2.34.2（urllib3 2.8.0）和 HTTPX 0.28.1（httpcore 1.0.9）；设置 `NODE_USE_ENV_PROXY=1` 的 Node.js 24.20.0 `fetch`；搭配 Chromium 153.0.8010.12 的 Playwright 1.63.0。[实验归档](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/wrong-version-number-lab.zip)包含[实验脚本](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/lab.py)、各客户端脚本、[README](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/README.md) 和[记录结果](https://ipvolt.com/downloads/fix-ssl-wrong-version-number-proxy/results.json)。

未测试：macOS 和 Windows、OpenSSL 与 Chromium 自带库之外的 TLS 库、SOCKS 代理、真正的 TLS 代理端口以及任何商业代理网络。直接关闭连接或保持沉默、不以明文应答的代理端口同样没有测试，可能产生不同的错误。

## 相关指南

- [代理环境变量：HTTP_PROXY 与 NO_PROXY](/zh/guides/proxy-environment-variables)说明 curl、Requests 和 Node 各自读取哪个变量。
- [在 curl 中使用代理](/zh/guides/curl-proxy-setup)涵盖 `-x`、`CONNECT` 隧道和 curl 的退出码。
- [配置 Python Requests 代理](/zh/guides/python-requests-proxy)讲解 `proxies` 字典和 `trust_env`。
- [HTTPX 异步代理](/zh/guides/httpx-async-proxy)涵盖显式客户端和连接池超时。
- [修复 ERR_TUNNEL_CONNECTION_FAILED 错误](/zh/guides/fix-err-tunnel-connection-failed)涵盖 Playwright 和 Puppeteer 中的代理拒绝。

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

## 参考来源与延伸阅读

- [urllib3: Your proxy appears to only use HTTP and not HTTPS](https://urllib3.readthedocs.io/en/stable/advanced-usage.html#https-proxy-error-http-proxy)
- [urllib3 issue #2564: the HTTP-only proxy hint shown for http:// proxy URLs](https://github.com/urllib3/urllib3/issues/2564)
- [urllib3 2.8.0 source: _wrap_proxy_error in connection.py](https://github.com/urllib3/urllib3/blob/2.8.0/src/urllib3/connection.py)
- [curl: -x, --proxy and the proxy URL scheme](https://curl.se/docs/manpage.html#-x)
- [curl: --write-out variables including http_connect](https://curl.se/docs/manpage.html#-w)
- [Requests: proxies](https://requests.readthedocs.io/en/latest/user/advanced/#proxies)
- [HTTPX: proxies](https://www.python-httpx.org/advanced/proxies/)
- [HTTPX: logging](https://www.python-httpx.org/logging/)
- [RFC 9110: the CONNECT method](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect)

## 相关指南

- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md)
- [在 curl 中使用代理：-x、环境变量、SOCKS5 与认证](https://ipvolt.com/zh/guides/curl-proxy-setup.md)
- [Python Requests 代理配置：认证与 SOCKS5](https://ipvolt.com/zh/guides/python-requests-proxy.md)

## 关于 ipvolt

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

[阅读英文原文](https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy.md)

## 了解何时开放体验。

ipvolt · 开发中

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

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

[申请抢先体验](https://ipvolt.com/zh/guides/fix-ssl-wrong-version-number-proxy#waitlist-closing)

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

