代理后的 pip check_hostname 报错:pip 还是隧道?
两种故障在代理背后打印相同的 pip 主机名错误。分清过旧的客户端与重新签名的代理,再修复正确的那一层。
两种不同的故障会打印相同的 pip 回溯,而适合其中一种的修复会让另一种更糟。客户端太旧、无法说出被隧道包裹的主机名,需要升级。代理会重新签发流量,需要受信任的根证书,而不是降级。改动代理变量之前先判断你遇到的是哪一种,因为两条路通向相反的方向。
动设置之前先读最后几帧
回溯停在哪里,说明哪一层放弃了。如果收尾帧位于 pip 自带 urllib3 的 TLS 代码内部,而文本抱怨主机名,那就是客户端库没能走完验证步骤,这远比证书问题更像版本问题。如果文本报告的是连接被拒、代理名无法解析或 HTTP 407,问题就在隧道端点,任何升级都改不了。
在失败的那个 shell 里同时抓取 pip 版本与代理变量。隧道起来之前打开的 shell 与之后打开的 shell 携带不同的值,两者的差异往往就是全部诊断。
python3 -m pip --version
env | grep -i -E 'proxy|no_proxy'
客户端比隧道更旧时
旧版 pip 自带的 HTTPS 栈会在套接字被隧道包住之后丢掉目标主机名。check_hostname 消息就是该栈在没有名字可比对时到达验证阶段发出的。升级能消除故障,因为较新的客户端会在整个握手过程中保留名字;只要收尾帧指向自带 TLS 代码,这个改动便宜到值得先试。
如果从出问题的机器访问不到索引,就在能访问的主机上取一个当前版本的 wheel,并从文件安装。用你真正下载到的文件名,不要用博客文章里抄来的版本号。
python3 -m pip install --upgrade pip
python3 -m pip install ./pip-24.2-py3-none-any.whl
代理重新签发流量时
终止 TLS 并在企业根下重新签发证书的代理会产生另一种错误:certificate verify failed,而不是主机名抱怨。这个差别就是线索,因为客户端已经连上服务器,然后拒绝了它被展示的身份。修复办法是显式信任那个根,可以只对一条命令生效,也可以通过 PIP_CERT 变量,这样客户端才能建立通往代理所呈现内容的信任链。把根加入系统信任库同样可行,但那会改变机器上每个程序的信任,比修好一次包安装是更大的决定。
把 --trusted-host 当诊断手段,绝不要当常态。它会关掉你本想保留的检查,于是真正错误的证书在这之后看起来是健康的。如果你曾用它装好某一个包,请移除它,并在下次发布之前修好信任链。
你自己的 Python 代码完全不看 pip 的配置。requests 调用需要把代理和证书包传到那个会话看得见的地方,所以下面的代码块把两者都设在会话对象上。
pip install --cert /usr/local/share/ca-certificates/corp-root.crt requests
export PIP_CERT=/usr/local/share/ca-certificates/corp-root.crt
s = requests.Session()
s.proxies = {"https": "http://10.20.30.40:8080"}
s.verify = "/usr/local/share/ca-certificates/corp-root.crt"
s.get("https://pypi.org/simple/", timeout=15)
端点或端口不对时
隧道客户端在连接时常常改写系统代理,这可能让 pip 指向一个在该端口上从不响应的主机。改任何 pip 设置之前,先用非 pip 的客户端测试端点。如果 curl 用同一个代理参数也失败,故障就在 pip 之上,pip 里的任何配置都救不了。
命令行开关与环境变量说的是同一件事,但作用范围不同:开关只对一次调用生效,变量对之后启动的每个进程生效。给内部索引加上 NO_PROXY,让私有包流量跳过绕行隧道的那段路。
curl -x http://proxy.corp.internal:8080 --head https://pypi.org/simple/
pip install --proxy http://proxy.corp.internal:8080 requests
把设置持久化,并证明路径可用
当代理需要活过当前 shell 时,把它存进 pip 自己的配置,然后再读回 pip 真正会用什么。debug 输出会点名每个值背后的文件与变量,这比翻文档更快地终结关于优先级的争论。
验证刻意使用两个客户端。标准库请求证明路由与信任链,下载则证明 pip 自己的配置与它们一致。当第一个成功而第二个失败时,差异在 pip 内部,而不在网络。
pip config set global.proxy http://proxy.corp.internal:8080
pip config debug
python3 -c "import urllib.request as r; print(r.urlopen('https://pypi.org/simple/').status)"
pip download --no-deps -d /tmp/wheelcheck requests
| What you see | First command | Repair |
|---|---|---|
| hostname message inside vendored TLS frames | python3 -m pip --version | upgrade the client |
| certificate verify failed after the tunnel | curl -x http://proxy.corp.internal:8080 --head https://pypi.org/simple/ | trust the corporate root |
| connection refused or HTTP 407 | env | grep -i -E 'proxy|no_proxy' | correct host, port or credentials |