编程语言

SSLEOFError: EOF occurred in violation of protocol(Python)

作者: · 最后核实

报错信息ssl.SSLEOFError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol

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

快速修复

最快的定位方法是在 Python 之外用 openssl 复现一次握手,看是谁挂断的:

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 检测工具或 openssl s_client -tls1_2 失败,而 -tls1 成功。
  • 修复: 在服务器上启用 TLS 1.2 和 TLS 1.3 以及现代加密套件。可以用 TLS 配置生成器生成 Nginx、Apache 配置;宝塔面板在网站的 SSL 设置里也能勾选 TLS 版本。

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

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

  • 确认: openssl s_client 不加 -servername 失败,加上就成功。
  • 修复: 始终传入主机名:
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。 如果发生在下载中途,数据可能已被截断;应该重试或明确报错。

常见问题

SSLEOFError 是证书问题吗?

不是。它表示连接被关闭了,而不是证书被拒绝,所以 verify=False 没有用。应该检查你和服务器之间有什么(代理、防火墙、负载均衡),以及双方各自支持哪些 TLS 版本和加密套件。

为什么 curl 正常,Python 却报错?

两者可能用的是不同的 OpenSSL 版本、TLS 版本、加密套件和代理设置。对比 curl -v 的输出和 python3 -c "import ssl; print(ssl.OPENSSL_VERSION)",检查 env | grep -i proxy,再用 openssl s_client -tls1_2 或 -tls1_3 测试具体版本。

只在并发高的时候才报错,说明什么?

多半是服务器、WAF 或 NAT 网关在拒绝或丢弃多出来的连接。用 requests.Session 复用连接、限制并发数,并配合退避重试(urllib3 的 Retry)。同时检查服务器端的连接数限制和限流规则,云厂商的 WAF、CC 防护也可能直接断开连接。

ssl.OP_IGNORE_UNEXPECTED_EOF 安全吗?

只能用于某个已知会不发 close_notify 就断开的服务器,而且协议本身要有长度标识(例如带 Content-Length 或 chunked 编码的 HTTP),这样数据被截断时仍能发现。只在专用的上下文里设置,不要全局开启。

资料来源

  1. Python docs: ssl — TLS/SSL wrapper for socket objects
  2. OpenSSL 3.0: openssl-s_client
  3. Requests: Advanced Usage
  4. pip: HTTPS Certificates