pip报错HTTPSConnectionPool?一文搞懂SSL证书验证失败与certifi修复
2026/9/9 18:02:55 网站建设 项目流程

1. 先把报错看明白:HTTPSConnectionPool 到底在对我喊什么

第一次看到这串报错的人,十有八九会对着屏幕愣住三秒钟。明明昨天pip install还是好的,今天一运行就吐出一大段 WARNING,最后以一句HTTPSConnectionPool收场。更让人烦躁的是,报错前面那几行“Retrying”看起来像是网络问题,可 Web 页面能打开、浏览器下载文件也正常,偏偏 pip 死活连不上 PyPI。我这里先给你看一个最典型的报错长相,然后逐行拆开讲清楚它到底在说什么。

WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)'))': /simple/pytest/ WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(...)': /simple/pytest/ WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(...)': /simple/pytest/ ... Could not fetch URL https://pypi.org/simple/pytest/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/pytest/ (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

注意看最后一行,HTTPSConnectionPool(host='pypi.org', port=443)这一句信息量很大。它表示 TCP 层面的连接实际上已经建立了,pip 已经成功连上了 pypi.org 的 443 端口,问题发生在后续的 TLS 握手阶段,也就是证书验证环节。很多人一看到“HTTPS”三个字就条件反射地去查防火墙、查代理、查 DNS,其实方向从一开始就偏了。Max retries exceeded只是在告诉你 pip 按默认重试策略连续失败多次后放弃了,它不是根因。

真正值得盯住的是这个关键字:

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

这句英文翻译成大白话就是:Python 在尝试用本地信任的 CA 证书库去验证 pypi.org 的证书链时,发现本地缺了某个证书,导致整条链拼不起来,于是直接拒绝连接,连 HTTP 请求都没发出去。你可能会问,为什么浏览器好好的、curl 也好好的,偏偏 Python 不行?这就要引出第二个问题了,先把它放在这里,后面我会专门展开说。

不同的证书报错其实有不同的“长相”,排查方向差别很大,我列个表方便你对照:

报错关键字实际含义常见场景
unable to get local issuer certificate本地证书库找不到能签发目标证书的根证书或中间证书企业网络设备替换了证书链、证书库不完整
self-signed certificate in certificate chain证书链里出现了一个自签证书,不受信任抓包工具、上网行为管理设备插入了自己的根证书
certificate has expired证书已过有效期系统时间错误、证书确实过期
hostname mismatch证书上的域名和实际访问的域名不匹配DNS 被劫持、走了错误的代理、hosts 被改

如果你遇到的报错属于self-signed certificate in certificate chain,那多半是某台设备在中间做了 TLS 解密再重新加密。这在国内办公网络里特别常见,后面几章我讲怎么应对。如果是hostname mismatch,那就要警惕是不是请求被导到了别的服务器上,这种属于安全事件,解决思路完全不同。

还有个非常迷惑人的现象:为什么“昨天还好好的,今天突然就不行了”。我遇到过的情况主要有这么几类:公司安全软件半夜自动升级,顺手往设备里部署了新的根证书;某次 Windows 系统更新把旧的根证书标记为不受信任;系统时间被偷偷改乱,导致证书验证时“当前时间”落到了有效期之外。所以排查时别急着只盯证书库,先看一眼时间对不对,这个检查成本最低,也最容易被人忽略。

2. 证书链验证的底层逻辑:Python 的信任库和浏览器不一样

想要彻底解决这类问题,不能只靠“照着命令敲一遍”,你得先理解 HTTPS 证书验证到底在验证什么。我打个比方:你去银行办事,柜员要核实你的身份证,身份证是公安局签发的,银行通过公安局的权威来确认你的身份。证书链也是同一个逻辑,只是链条更长:根证书相当于公安局,中间证书相当于省公安厅,服务器证书相当于你手里那张身份证。

正常情况下,pypi.org 的服务器会返回自己的服务器证书,同时附上签发它的中间证书。客户端(也就是你本机)拿到这份证书后,会从服务器证书向上追溯签发者,直到找到一个本地已经信任的根证书,再从根证书出发,逐级校验签名,全部通过才算验证成功。问题就出在这个“本地已经信任的根证书”上。不同的软件,信任的根证书列表来源可能完全不一样。

这里要澄清一个很多人搞错的关键点。pip 在底层使用的是 urllib3 这个 HTTP 库,而 urllib3 在绝大多数 Python 环境里会优先读取certifi这个包提供的cacert.pem文件,而不是直接使用操作系统自带的证书库。也就是说,即使你在 Windows 的“受信任的根证书颁发机构”里装好了某个企业根证书,浏览器和 curl 都认了,pip 也照样不认,因为它是从自己的cacert.pem里找根证书的。

这是我在实际排查中最大的心得,也是最容易让新手绕弯路的地方。很多人折腾半天,往系统证书库里导入了证书,浏览器访问正常了,以为问题解决了,结果一回终端pip install还是原封不动的报错,然后就开始怀疑人生。

Python 的证书查找机制大致可以按平台归纳一下:

环境证书来源排查优先级
绝大多数 pip 安装场景urllib3 优先读certificacert.pem最高优先级
Python ssl 模块本身编译 OpenSSL 时的默认 CA 路径(ssl.get_default_verify_paths()第二优先级
Windows 系统ssl 模块部分场景会加载系统证书库辅助参考
macOSPython 安装器附带Install Certificates.command脚本需要手动触发

所以排查证书问题的时候,第一步不是去看浏览器,而是先搞清楚你当前的 Python 环境里 pip 到底在读哪个证书文件。这一步没搞清楚,后面所有操作都可能是在对着空气打拳。

回到“为什么浏览器正常、pip 异常”的问题。浏览器用的是操作系统证书库,Windows 也好、macOS 也好,都会自动从系统更新里同步证书;而 Python 的 certifi 包相当于一个独立的“私房证书库”,它不会跟着系统自动同步。如果企业网络里某台安全网关在中间插入了自己的证书,Python 的证书库里可没有这个企业根证书,验证自然就失败了。

还有一类非常典型的环境是conda环境的 Python。conda 为了方便跨平台,会自带一份 OpenSSL 库,它的默认 CA 路径指向miniconda3\Library\ssl\cacert.pem这类位置,和系统 Python 的证书完全不是同一份。于是你会看到同一个机器上,系统 Python 装的包都能正常下载,conda 环境里的 pip 却一直报 SSL 错误,两个环境各管各的,互相看不见。我在内网帮同事排查时经常遇到这种局面,处理方式也很简单:分别定位每个环境自己的证书库,分别处理。

理解了这个底层机制,你再看网上一堆排错帖子,就不会觉得乱了。有的人说“更新 certifi 就好”,那是因为他的环境确实缺的是公共根证书;有的人说“把证书装进系统库就行”,那也只有在 curl 类软件报错时才成立。对 pip 而言,真正的关键路径始终是 certifi 的cacert.pem和你 Python 环境里 ssl 模块默认读取的 CA 目录。

3. 逐级修复:从临时绕过到把证书真正装进信任库

这一章给完整实操,按“救急 → 定位 → 根治”的顺序走。每一步我都会告诉你要么这样做、要么那样做的判断依据,让你不仅会敲命令,还能知道自己为什么要敲。

3.1 救急方案:--trusted-host 临时绕过证书验证

如果你现在被报错卡住,急需装一个包干活,最快的办法是:

pip install pytest --trusted-host pypi.org --trusted-host files.pythonhosted.org

--trusted-host的作用是告诉 pip:这个域名我信得过,不用验证证书,直接下载。这里的files.pythonhosted.org是 PyPI 真正存储安装包文件的域名,只加 pypi.org 往往不够,因为下载阶段访问的是 files.pythonhosted.org 这个子域。如果你是用了国内镜像源,可以把域名替换成镜像源地址:

pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn

强调一句:--trusted-host只适合救急,绝对不要当成长期方案写在 pip.conf 里长期使用。它的本质是关闭 TLS 证书校验,等于放弃了 HTTPS 的防篡改能力,中间任何人理论上都能往你下载的包里塞东西。你临时用它装一个包应急没问题,但这背后真正的证书问题并没有解决,等下一个环境、下一台机器出现同样问题时,还得重新折腾一遍。

3.2 定位:你的 pip 到底在读哪个证书库

救急之后回到正题。先跑这三条命令,把当前环境的证书路径摸清楚:

python -c "import ssl; print(ssl.get_default_verify_paths())" python -c "import certifi; print(certifi.where())" python -c "import urllib3; print(urllib3.util.ssl_.DEFAULT_CERTS)"

第一条命令会输出 OpenSSL 编译时设定的默认 CA 路径,包括openssl.cnf的位置;第二条输出 certifi 包的cacert.pem具体路径;第三条确认 urllib3 实际使用的默认证书文件。这三条输出合并起来看,基本就能确定 pip 是会读 certifi 文件,还是会退回到 OpenSSL 默认路径。

在大多数情况下,输出结果里会有一个类似C:\Python39\lib\site-packages\certifi\cacert.pem/usr/local/lib/python3.9/site-packages/certifi/cacert.pem的路径。对 pip 来说,这个文件才是“主战场”。你先把这个路径记下来,后面所有追加操作都在这个文件上做。

3.3 看链:用 openssl 拉出服务器真正返回的证书链

不要急着把证书乱加一通。先用一条命令看看 pypi.org 服务器在面对你当前的网络环境时,到底返回了怎样的证书链:

openssl s_client -connect pypi.org:443 -showcerts -servername pypi.org

这条命令会建立一条真实的 TLS 连接,然后把服务端在握手时发送的整条证书链打印出来。输出里会有多个-----BEGIN CERTIFICATE-----段,每一段是一张证书。如果你在企业网络里开启了某个安全设备,这里的证书链大概率不是 PyPI 官方的 Let's Encrypt 证书,而是那个设备自己签发的证书。看清楚“服务端发来的链”和“你本地 truststore 里的链”到底差在哪一级,才能决定要补哪张证书。

实际操作中我发现,很多时候不是根证书缺失,而是中间证书缺失。企业网关在替换证书时只会下发自己的叶子证书和根证书,中间证书没给全,客户端拼不出完整链。这种情况光往 certifi 里追加根证书也没用,还要把中间证书一并补上,顺序通常是“叶子证书在前,中间证书在后,根证书在后”,但追加到 cacert.pem 里的顺序本身并不敏感,只要三张都在就行。

3.4 根治:把企业 CA 证书追加进 certifi 并让 pip 识别

拿到需要补的证书之后,要确认它是 PEM 格式。什么是 PEM 格式?就是文本文件开头写着-----BEGIN CERTIFICATE-----的那种。如果拿到的证书是.cer.crt的二进制 DER 格式,先用 OpenSSL 转换:

openssl x509 -inform DER -in company.cer -out company.pem

转成 PEM 之后,先备份原文件,再追加。Windows 环境:

type company.pem >> C:\Python39\Lib\site-packages\certifi\cacert.pem

Linux 环境:

cat company.pem >> /usr/local/lib/python3.9/site-packages/certifi/cacert.pem

追加完之后,最好再验证一下文件末尾有没有多余的空行导致 PEM 块不能正确切割,这个坑我踩过。用tail -n 5 cacert.pem看一下最后几行,确保结尾直接是-----END CERTIFICATE-----换行结束。

如果你不想直接改 certifi 的源文件,还有一个更灵活的方式:用环境变量指定 CA 文件。Python 的 ssl 模块和 urllib3 都认SSL_CERT_FILE这个环境变量,requests 库则额外认REQUESTS_CA_BUNDLE

set SSL_CERT_FILE=D:\certs\company-ca.pem set REQUESTS_CA_BUNDLE=D:\certs\company-ca.pem pip install pytest

这种方式的好处是,你可以把企业 CA 单独放一个文件管理,不想用的时候直接删环境变量就行,不用去改动 certifi 文件。缺点是每个新终端窗口都要重新设置,比较麻烦。如果你确定这个环境长期要在企业内部网络使用,直接改 certifi 的 cacert.pem 反而更干净,毕竟它是一个 base64 文本文件,追加内容不会对文件原有结构造成破坏。

3.5 系统级证书库:强烈建议同时处理

虽然 pip 不读系统证书库,但我还是建议你把企业根证书同时装进系统库里。原因很简单:你会用到的工具不止 pip,curl、git、各种 IDE 的插件、Node.js 的 npm,它们读取的可能就是系统证书库。只解决 pip 一个问题,等于治标不治本。

Windows 上双击.cer文件,选择“安装证书”,存储位置选“本地计算机”,然后把证书放到“受信任的根证书颁发机构”。Linux 上:

sudo cp company.pem /usr/local/share/ca-certificates/ sudo update-ca-certificates

这里再给一个额外的经验:如果你在公司内网搭了私有 PyPI 源,服务器证书也是自签的,同样按照上面的办法,把私有源 CA 追加到 certifi 就行。不用为了私有源特意关掉全局证书验证,那样风险太大。

4. 从报错到恢复的完整排查链路复盘

理论和操作都说完了,这一章我拿一次真实的排查过程做个完整复盘。某天我在办公网络下想装一个新包,一运行就回退到HTTPSConnectionPool报错,跟第一章开头的报错一模一样。整个排查过程我按下面这个顺序走,每一步都有明确的判断依据。

第一步,先看范围。我打开终端跑了一句curl https://pypi.org/simple/,结果 curl 居然成功返回了页面。这一步直接帮我排除了系统层面的网络封锁问题,把矛头锁定在 Python 自身的证书库上。如果 curl 也报错,那说明问题出在系统证书链或者 DNS 解析上,排查方向就要往系统层面延伸。

第二步,检查代理配置。运行pip config listecho %HTTPS_PROXY%,确认 pip 有没有走企业代理。在办公网络里,经常有人之前在 IDE 里配置过代理,后来系统代理变了但环境变量还是旧的,导致 pip 请求被导到一个不该去的地方。这一步排除了代理干扰。

第三步,看 DNS 解析结果。执行nslookup pypi.org,确认解析到的是公网 IP 而不是内网地址。这一步是为了防止公司 DNS 私自把公网域名解析到内网网关,导致我实际连接的是一个“假的 pypi.org”。

第四步,执行python -c "import certifi; print(certifi.where())",确认当前 pip 读的是哪个 cacert.pem 文件。同时用openssl s_client -connect pypi.org:443 -showcerts拉出了服务端证书链,发现链上的证书确实是由公司安全网关自签的,跟官方 PyPI 证书完全不一样。

第五步,导出公司的根证书,转换成 PEM 格式,追加到 certifi 的 cacert.pem。这里我特意先在一台干净的测试机器上验证了流程,确认无误后才在要用的环境里操作。

第六步,重新执行pip install,这次直接成功,没有重试、没有警告。

把整个过程整理成一张排查表,方便你以后遇到同类问题时对照执行:

检查项命令结果判断
系统层证书curl https://pypi.org/simple/失败→系统证书库问题;成功→Python证书库问题
代理环境变量echo %HTTPS_PROXY%/pip config list有值→确认代理是否可信
DNS 解析nslookup pypi.org解析到内网 IP→检查 DNS 和 hosts
Python证书路径python -c "import certifi;print(certifi.where())"确认 pip 到底读哪个文件
服务端证书链openssl s_client -connect pypi.org:443 -showcerts对比本地信任库缺哪个环节

除了上面这个标准流程,我再分享几个特殊情况。有一次我排查了半天,发现不是证书库的问题,而是系统时间快了六个小时,证书还在“未来有效期”,被判定为无效。另一次是 conda 环境和系统 Python 并存,系统 Python 修好了,conda 环境里 pip 依旧报错,因为 conda 环境用的 OpenSSL 路径完全不同,得单独再处理一遍。这类问题本质都一样,只是证书库位置不一样,很多人忽略了“同一个机器上可能存在多套 Python”这个事实。

5. 别让证书问题反复找上门:长期预防经验

问题解决了,但我建议你多做一步:想想怎么让它别再发生。说句实在话,证书问题属于“环境类坑”,不像业务代码那样修一次就彻底消失。换台电脑、装个新环境、公司安全策略调整,都会让它再冒出来。所以我把长期预防的一些经验写在最后,给你作参考。

我现在的做法是,新装一个 Python 环境之后,会把下面这几件事做成一套固定流程:

  1. 升级 pip 到最新版,旧版 pip 的报错信息比较晦涩,升级后信息更清晰,排查起来省不少时间。
  2. 先跑python -c "import certifi; print(certifi.where())"确认 certifi 文件路径存在。
  3. 如果在企业网络环境,主动把企业根证书追加到 cacert.pem 里,不等报错再处理。
  4. 顺手配置国内镜像源(清华、阿里都可以),省得每次都在 PyPI 官方源上浪费下载时间。配镜像源一样要关心证书问题,镜像源自己的证书如果过期或不被信任,同样会报 HTTPSConnectionPool。

再提醒一句,使用 venv 隔离环境时有个容易被忽略的细节:同一个 Python 解释器创建的多个 venv 是共享同一份 certifi 包的,所以你在基础环境里追加了证书,所有从这个解释器创建的 venv 都能直接识别。但 conda 环境是各管各的,每个 conda 环境都有自己独立的 certifi 和 OpenSSL 配置,换了环境就要重新处理。这也是为什么我遇到 conda 用户时会多问一句“你到底在哪个环境里跑的 pip”。

还有一个非常实用的建议:不要把所有证书都塞到 cacert.pem 里。我见过有人把三四个不同公司的证书全加进去,加到最后文件混乱不堪,出了新问题反而不知道是哪张证书在生效。更稳妥的方式是单独维护一个company-ca.pem文件,用SSL_CERT_FILE环境变量指向它,这样环境和证书解耦,排查问题的时候把环境变量一去掉就切回默认状态,方便对比验证。

另外,pip install --trusted-host这种救急命令,我建议只在确认“临时装一个无关紧要的包”时使用,用完就把 pip.conf 里对应的配置删掉。你可能会觉得无所谓,但证书验证机制是有安全意义的,它可以防止下载的包被中间人替换。一旦长期关闭,等于给攻击者留了一扇门。

最后说一个我自己的习惯。每次遇到HTTPSConnectionPool报错,我不再焦虑,也不急着搜答案,而是按“时间对不对 → 代理有没有 → certifi 路径是什么 → 服务端返回了什么证书链”这个顺序过一遍,基本五分钟内就能定位。这套思路完全可以复制到你自己的工作流里,毕竟证书问题不可怕,可怕的是没有一套稳定的排查方法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询