# SSLEOFError: EOF occurred in violation of protocol（Python）

> SSLEOFError 表示 TLS 连接在握手中途或没有正常关闭就被断开。应排查代理、SNI、TLS 版本和服务器限制，而不是关闭证书校验。

Source: https://ssl.sh/zh/errors/python-ssl-eof-occurred-in-violation-of-protocol/
Last updated: 2026-11-10

**`SSLEOFError: EOF occurred in violation of protocol` 的意思是：TLS 连接被中途掐断了，对端或中间设备没有按 TLS 规范收尾就关闭了 TCP 连接。** 如果发生在握手阶段，通常指向代理或防火墙、TLS 版本或加密套件不匹配、缺少 [SNI](https://ssl.sh/zh/glossary/sni/) 主机名；如果发生在读取响应时，通常是服务器没发 close_notify 就关闭了连接。

## 快速修复

最快的定位方法是在 Python 之外用 openssl 复现一次[握手](https://ssl.sh/zh/learn/tls-handshake/)，看是谁挂断的：

```bash
openssl s_client -connect example.com:443 -servername example.com </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
env | grep -i _proxy
```

- 输出 `unexpected eof while reading` 或 `no peer certificate available`，服务器证书一张都没收到：连接在握手中被关闭。换个网络、去掉代理变量再测。
- 只有加 `-tls1` 或 `-tls1_1` 才能握手成功：服务器太老，现代 Python 不再支持，需要升级服务器的 TLS 配置。
- 握手正常，但代码在下载时报错：见原因 4。

## 原因

这个报错有 5 类原因：握手阶段断开多是代理、TLS 版本或 SNI 问题，读响应时断开多是服务器没发 close_notify。

### 1. 代理、防火墙或安全网关断开了连接

公司安全网关可能按 SNI 拦截某些目标地址，直接重置连接；代理变量指向了错误的端口，效果也类似。

- **确认：** 换个网络，或设置 `NO_PROXY=example.com` 后报错消失；`openssl s_client` 输出中服务器始终没发证书。
- **修复：** 使用正确的代理地址（一般以 `http://` 开头），或请网络管理员放行目标地址。

### 2. 服务器不支持你的 TLS 版本或加密套件，直接挂断

多数服务器会返回协议告警，但老旧设备、嵌入式系统和配置不当的负载均衡会直接关闭连接。从 Python 3.10 起，默认上下文要求 TLS 1.2 以上和安全级别 2，只支持旧协议的服务器就连不上了。

- **确认：** [SSL 检测工具](https://ssl.sh/zh/tools/ssl-checker/)或 `openssl s_client -tls1_2` 失败，而 `-tls1` 成功。
- **修复：** 在服务器上启用 TLS 1.2 和 TLS 1.3 以及现代加密套件。可以用 [TLS 配置生成器](https://ssl.sh/zh/tools/tls-config-generator/)生成 Nginx、Apache 配置；宝塔面板在网站的 SSL 设置里也能勾选 TLS 版本。

### 3. 没有发送 SNI，或主机名服务器不认识

一台服务器托管多个站点时，收到不带 SNI 或 SNI 不匹配的连接可能直接关闭。调用 `wrap_socket()` 时没传 `server_hostname=`，或者直接用 IP 连接，都不会发送 SNI。

- **确认：** `openssl s_client` 不加 `-servername` 失败，加上就成功。
- **修复：** 始终传入主机名：

```python
import socket, ssl
ctx = ssl.create_default_context()
with socket.create_connection(("example.com", 443)) as sock:
    with ctx.wrap_socket(sock, server_hostname="example.com") as tls:
        print(tls.version())
```

### 4. 服务器返回响应后没发 close_notify 就关闭

OpenSSL 3 会把缺少 close_notify 明确报告为错误（`UNEXPECTED_EOF_WHILE_READING`），取决于库读取响应体的方式，下载可能以 SSLEOFError 结束。

- **确认：** 握手成功，大部分数据也收到了，错误出现在读取的末尾。
- **修复：** 修改服务器或代理，让它正常关闭 TLS。实在改不了，只在访问该服务器的专用上下文里设置 `ssl.OP_IGNORE_UNEXPECTED_EOF`（Python 3.10+、OpenSSL 3）。

### 5. 服务器端的限制

WAF、CDN、过载的后端在达到限制时会丢弃新连接；要求双向认证（mTLS）的服务器在客户端没提供证书时也可能直接关闭连接。

- **修复：** 复用连接、退避重试并查看服务器日志；mTLS 场景用 `cert=`（requests）或 `load_cert_chain()` 提供客户端证书。

## 各环境修法

各工具的修法都围绕三件事：代理设置写对、TLS 版本和 SNI 符合服务器要求、偶发断开时复用连接并重试。

**pip：** `pip install --proxy http://proxy.example.com:8080 package`。如果是某个镜像源或私有仓库触发报错，先用 `openssl s_client` 单独测试它；国内常用的清华、阿里云镜像都支持 TLS 1.2 以上，换源前后都应保持 `https://`。

**requests：** 使用 `Session`，挂载带 `max_retries` 的 `HTTPAdapter` 应对偶发断开；环境变量不可靠时显式传入 `proxies=`。

**Docker / CI：** 确认容器里拿到的代理变量和你的 shell 一致，并检查容器里的 OpenSSL 版本是否符合预期（`python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"`）。

**无法升级的老设备：** 只为这一台主机单独创建上下文，并写清原因，不要改全局默认值。

## 不要这样做

这些做法要么对断连无效，要么削弱进程里所有连接的安全性。

- **`verify=False`。** 对连接被关闭没有帮助，还会让其他所有请求失去证书校验。
- **全局降低最低 TLS 版本或安全级别**（例如 `DEFAULT:@SECLEVEL=0`）。为了迁就一台坏掉的服务器，削弱了进程里的所有连接。
- **用空的 `except` 吞掉 SSLEOFError。** 如果发生在下载中途，数据可能已被截断；应该重试或明确报错。
