编程语言

SSLError: Max retries exceeded:requests 握手失败怎么修

作者: · 最后核实

报错信息requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url 的意思是:和服务器的 TLS 握手失败了,requests 放弃了这次请求。 requests 默认不重试,握手失败一次就会报这条错,“Max retries exceeded”与重试次数无关。具体原因写在报错末尾的 (Caused by SSLError(...)) 里。

这条报错是什么意思

这条报错来自 requests 底层的 urllib3:urllib3 建立 TLS 连接失败时会抛出 MaxRetryError,它的文字固定是 Max retries exceeded with url: <路径> (Caused by <原因>),前面再加上连接池的名字,比如 HTTPSConnectionPool(host='example.com', port=443)。如果原因属于 SSL 错误,requests 就把它包装成 requests.exceptions.SSLError 抛出来。requests 默认的 HTTPAdapter 用的是 Retry(0, read=False),所以这里的“最大重试次数”是零次,第一次失败就结束了。

把完整报错拉到最后看,常见的几种形态如下(_ssl.c 后面的行号随 Python 版本变化):

(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')))
(Caused by SSLError(SSLCertVerificationError(1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'example.com'. (_ssl.c:1006)")))
(Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1006)')))
(Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1006)')))

方括号里的 CERTIFICATE_VERIFY_FAILED、WRONG_VERSION_NUMBER 是 OpenSSL 的原因代码,按它对照下面的原因逐个排查即可。

常见原因(按出现频率排序)

这条报错里最常见的是证书验证失败(CERTIFICATE_VERIFY_FAILED),其余 4 种是域名不匹配、代理写错、连接被断开和服务器 TLS 过旧。

1. 证书验证失败

CERTIFICATE_VERIFY_FAILED 后面跟着 unable to get local issuer certificate 或 self-signed certificate in certificate chain。原因要么是服务器没把中间证书一起发过来,要么证书由当前环境不信任的 CA 签发:公司内网 CA、做 HTTPS 解密的上网行为管理设备,或者比你的 certifi 版本更新的公开根证书。

  • 确认: 运行 openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null,只看到 depth=0,或者校验码是 20、21。也可以把域名放进 SSL 检测工具从外部看证书链。
  • 修复: 证书链不完整就在服务器上配置完整链(见缺少中间证书,宝塔面板里要粘贴包含中间证书的内容)。签发者是内网或代理 CA,就给 requests 指定包含该根证书的证书包。完整步骤见 Python CERTIFICATE_VERIFY_FAILED。

2. 域名不匹配

Hostname mismatch, certificate is not valid for 'example.com' 表示证书链可信,但证书的 SAN(使用者可选名称)里没有你访问的这个名字。常见于证书只签了 www,代码却访问裸域名,或者直接用 IP 访问。

  • 确认: openssl s_client -connect example.com:443 -servername example.com </dev/null | openssl x509 -noout -ext subjectAltName。
  • 修复: 改成证书覆盖的域名,或者重新申请包含该名字的证书。

3. 代理配置写错

WRONG_VERSION_NUMBER 一般说明 TLS 数据发给了一个用明文 HTTP 回应的对象。在 requests 里最常见的是把 HTTPS_PROXY 写成了 https://proxy.example.com:3128,而这个代理只支持普通 HTTP;或者请求打到了 HTTP 端口。

  • 确认: env | grep -i proxy,对照代理实际支持的协议。
  • 修复: 普通的 HTTP CONNECT 代理,地址要写成 http://。详见 Python SSL WRONG_VERSION_NUMBER。

4. 握手过程中连接被断开

SSLEOFError / UNEXPECTED_EOF_WHILE_READING 表示对方或者中间设备没发 TLS 告警就直接关了 TCP 连接。防火墙、限流、服务器过载、服务器要求 SNI 或更新的协议版本都会导致这种情况。见 EOF occurred in violation of protocol 和 unexpected eof while reading。

5. 服务器 TLS 配置过旧

OpenSSL 3 会直接拒绝一些老旧服务器,原因代码会写明:不支持 RFC 5746 的服务器报 UNSAFE_LEGACY_RENEGOTIATION_DISABLED,DH 参数太弱报 DH_KEY_TOO_SMALL。这类问题应该在服务器端修。

国内环境的几种典型情况

国内最常见的是三种情况:公司网络解密 HTTPS、本机代理软件改了环境变量、内网服务用自签证书。

公司电脑或办公网络: 很多单位的上网行为管理、终端安全软件会解密 HTTPS,再用自己的根证书重新签名。浏览器因为信任了系统里装好的根证书而一切正常,requests 只认 certifi,于是报 CERTIFICATE_VERIFY_FAILED。找 IT 要这张根证书,按下面的方法加进证书包即可,不要让整个项目关闭校验。

本机开着代理软件: 有些代理工具会自动设置 HTTP_PROXY、HTTPS_PROXY 环境变量。代理关掉了、端口变了、或者协议写成了 https://,requests 都会报这条错。先运行 env | grep -i proxy 看清楚当前生效的是什么。

服务器在内网、证书是自己签的: 这种情况最适合把内网根证书装进系统,再设置 REQUESTS_CA_BUNDLE,一次配置,所有脚本都能用。

分平台修复命令

正确做法是给 requests 指定正确的 CA 证书包,而不是关闭校验。verify 保持默认时,requests 依次读取 REQUESTS_CA_BUNDLE 和 CURL_CA_BUNDLE。

Debian / Ubuntu / Docker 镜像:

apt-get update && apt-get install -y ca-certificates
cp company-root.crt /usr/local/share/ca-certificates/company-root.crt
update-ca-certificates
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

CentOS / RHEL / Rocky / 阿里云 Alibaba Cloud Linux:

cp company-root.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust
export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt

macOS 和 Windows: 用 truststore(Python 3.10 及以上)直接读取操作系统证书库,公司 IT 通过域策略下发的根证书也能被识别:

import truststore
truststore.inject_into_ssl()   # 在创建 session 之前调用一次

import requests
requests.get("https://example.com")

国内装包: 安装 truststore、certifi 时 pip 很慢,可以临时用清华或阿里云镜像,地址必须写 https://,不要加 --trusted-host,那个参数会让 pip 对该主机放弃 HTTPS 校验:

pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple truststore
# 或
pip install -i https://mirrors.aliyun.com/pypi/simple/ truststore

在代码里单独指定:

import requests
requests.get("https://intranet.example.com", verify="/etc/ssl/certs/company-bundle.pem")

不推荐:关闭证书校验

verify=False 只是把报错变成一条 InsecureRequestWarning,同时去掉了“确认对方是真服务器”这道检查。网络路径上的任何一方,比如被入侵的路由器、公共 Wi-Fi、恶意代理,都可以读取或篡改流量,接口密钥、Cookie、密码都会暴露。改写 ssl._create_default_https_context 效果一样,而且会影响整个进程里的所有库。

更安全的替代办法:内网 CA 用 verify="/path/to/bundle.pem";整个环境统一用 REQUESTS_CA_BUNDLE;根证书已经装进系统就用 truststore;缺中间证书就让服务器补全证书链。本地测试自签名服务器时,也应该只信任那一张证书文件,而不是整体关闭校验。

怎么确认已经修好

python3 - <<'EOF'
import requests
r = requests.get("https://example.com", timeout=10)
print(r.status_code, r.url)
EOF

脚本应当正常打印状态码,不抛 SSLError,也不出现 InsecureRequestWarning。再确认你设置的证书包路径确实存在(ls -l "$REQUESTS_CA_BUNDLE"),路径写错时 requests 会报 Could not find a suitable TLS CA certificate bundle,容易和这条报错混淆。最后用 SSL 检测工具复查服务器端;想弄清楚为什么缺一张中间证书浏览器没事、Python 却失败,可以读证书链原理。

常见问题

加大重试次数(Retry 或 HTTPAdapter(max_retries=...))能解决吗?

不能。重试只对偶发的网络抖动有用。缺中间证书、根证书不受信任、代理协议写错,这些问题每次都会以同样方式失败,重试多少次都一样。先修 Caused by 后面的原因,确属偶发的超时再考虑加重试。

为什么浏览器和 curl 能打开,requests 却报错?

三者用的信任库不同:浏览器用操作系统证书库,还会自动补下载缺失的中间证书;curl 用编译时指定的系统证书包;requests 默认只用 certifi。先用证书链补全工具看服务器证书链是否完整,链没问题的话,就让 requests 使用和其他工具一样的证书包。

怎么判断是代码的问题还是服务器的问题?

在同一台机器上运行 openssl s_client -connect example.com:443 -servername example.com。结果是 Verify return code: 0 (ok) 说明服务器没问题,要修的是 Python 环境的 CA 证书包;结果是 20 或 21,说明服务器没发完整证书链,只能由服务器管理员修。

用了国内 PyPI 镜像,pip 能装包,为什么 requests 还报错?

pip 和你的代码访问的是不同的地址。镜像只影响 pip 下载包,不影响 requests 访问业务接口。另外 pip 24.2 起在 Python 3.10+ 上会同时使用系统证书和 certifi,而 requests 默认只用 certifi,所以同一台机器上两者的表现可能不一样。

资料来源

  1. Requests: Advanced Usage (SSL Cert Verification)
  2. urllib3 source: exceptions.py (MaxRetryError)
  3. Requests source: adapters.py (DEFAULT_RETRIES, SSLError)
  4. Python docs: ssl — TLS/SSL wrapper for socket objects
  5. 清华大学开源软件镜像站 PyPI 帮助