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

> requests 同时报 SSLError 和 Max retries exceeded，表示 TLS 握手失败，与重试次数无关。按 Caused by 后面的原因分别修证书链、CA 证书包或代理，不用 verify=False。

Source: https://ssl.sh/zh/errors/python-requests-max-retries-ssl-error/
Last updated: 2026-11-10

**`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 版本变化）：

```text
(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 检测工具](https://ssl.sh/zh/tools/ssl-checker/)从外部看证书链。
- **修复：** 证书链不完整就在服务器上配置完整链（见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)，宝塔面板里要粘贴包含中间证书的内容）。签发者是内网或代理 CA，就给 requests 指定包含该根证书的证书包。完整步骤见 [Python CERTIFICATE_VERIFY_FAILED](https://ssl.sh/zh/errors/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](https://ssl.sh/zh/errors/python-ssl-wrong-version-number/)。

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

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

### 5. 服务器 TLS 配置过旧

OpenSSL 3 会直接拒绝一些老旧服务器，原因代码会写明：不支持 RFC 5746 的服务器报 [UNSAFE_LEGACY_RENEGOTIATION_DISABLED](https://ssl.sh/zh/errors/python-unsafe-legacy-renegotiation-disabled/)，DH 参数太弱报 [DH_KEY_TOO_SMALL](https://ssl.sh/zh/errors/openssl-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 镜像：**

```bash
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：**

```bash
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 通过域策略下发的根证书也能被识别：

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

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

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

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

**在代码里单独指定：**

```python
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；缺中间证书就让服务器补全证书链。本地测试自签名服务器时，也应该只信任那一张证书文件，而不是整体关闭校验。

## 怎么确认已经修好

```bash
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 检测工具](https://ssl.sh/zh/tools/ssl-checker/)复查服务器端；想弄清楚为什么缺一张中间证书浏览器没事、Python 却失败，可以读[证书链原理](https://ssl.sh/zh/learn/certificate-chains/)。
