最近遇到的一个问题,在搞清楚之后才发现这么多年的 HTTPS_PROXY 都配置错了!

起因

想用 Python 在网上下载一些图片素材,结果 requests 报错 requests.exceptions.ProxyError

具体的错误信息见下面。当然第一时间是把系统代理关了,结果访问就正常了。

如果只是这样,可能我就觉得是代理有问题,然后关了用就行了,但是偏偏想要下载的资源里是必须要走代理的,所以只能想办法解决。

下面先介绍一下具体的情况:

解决过程

操作系统:Windows 10

Python: 3.8(有虚拟环境)

requests 通过代理访问外网时报错如下:

Traceback (most recent call last):
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\connectionpool.py", line 696, in urlopen
self._prepare_proxy(conn)
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\connectionpool.py", line 964, in _prepare_proxy
conn.connect()
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\connection.py", line 359, in connect
conn = self._connect_tls_proxy(hostname, conn)
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\connection.py", line 496, in _connect_tls_proxy
return ssl_wrap_socket(
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\util\ssl_.py", line 432, in ssl_wrap_socket
ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)
File "E:\code\Python\.venv\smalltools\lib\site-packages\urllib3\util\ssl_.py", line 474, in _ssl_wrap_socket_impl
return ssl_context.wrap_socket(sock)
File "C:\Users\Davy\AppData\Local\Programs\Python\Python38\lib\ssl.py", line 500, in wrap_socket
return self.sslsocket_class._create(
File "C:\Users\Davy\AppData\Local\Programs\Python\Python38\lib\ssl.py", line 1041, in _create
self.do_handshake()
File "C:\Users\Davy\AppData\Local\Programs\Python\Python38\lib\ssl.py", line 1310, in do_handshake
self._sslobj.do_handshake()
OSError: [Errno 0] Error

因为浏览器访问是没有问题的,代理本身应该没有问题。

按照这个错误信息在网上搜了一下,比较接近的帖子给的解决方案有安装 ssl 模块之类,都照着检查了一遍,问题还是没有解决。

因为网上的内容有些年头了,并且我觉得使用代理是非常常见的场景,既然没多少人报这个问题,那么很可能只是偶然的 bug,于是想着把版本再升级试试。

升级到 python 3.9 ,错误仍然存在,提示略有变化:

Traceback (most recent call last):
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 696, in urlopen
self._prepare_proxy(conn)
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 964, in _prepare_proxy
conn.connect()
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connection.py", line 359, in connect
conn = self._connect_tls_proxy(hostname, conn)
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connection.py", line 496, in _connect_tls_proxy
return ssl_wrap_socket(
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\ssl_.py", line 432, in ssl_wrap_socket
ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\ssl_.py", line 474, in _ssl_wrap_socket_impl
return ssl_context.wrap_socket(sock)
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 500, in wrap_socket
return self.sslsocket_class._create(
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1040, in _create
self.do_handshake()
File "C:\Users\Davy\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1309, in do_handshake
self._sslobj.do_handshake()
ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:1122)

好歹错误信息有点变化,于是按照最下面 ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:1122) 去谷歌,并没有找到解决办法,但是发现有人在不久前遇到了相同的问题,并且通过降级 Python 3.7 解决了。

参见:https://v2ex.com/t/738031

先重新安装 Python 3.7 试了一下果然可行,并且意外地发现在 Python 3.8 环境下也是可行的,也就是可以排除 Python 版本的问题,那么自然就怀疑是某个包引发的。

通过简单地对比和排除,很快就发现了问题所在:

模块 urllib3 的版本,报错的是 1.26.3,没报错的是 1.25.11

在原报错环境中使用下面命令重装低版本 urllib3

pip install urllib3==1.25.11

然后测试果然就没问题了。

问题根源

先查了一下 urllib3 的更新日志,应该是 1.26.0 的修改导致的:

按照这个更新日志,明明应该是增加了 HTTPS 的支持,怎么反而让它失效了呢?

我一时搞不明白这个问题,但是想起了我最近遭遇到了另一个问题,然后意外地找到了真相:

同样遭遇代理错误的 pip

同样是在这个环境中,其实在一开始我就遭遇了 pip install 安装包失败的问题,报错信息是:

​ 'ProxyError('Cannot connect to proxy.', FileNotFoundError(2, 'No such file or directory'))'

同样是取消系统代理就能正常安装,就没太在意了。

但是在降级 urllib3 解决了 requestsProxyError 之后,我开始怀疑 pip 安装是不是也是这个问题呢?

直接在降级了 urllib3 的环境中测试了一下,错误仍然存在,但是版本整体较低的环境中,是没有问题的!

于是继续对比版本包,结果在 pip 包的路径下发现有一个 _vendor\urllib3 目录,原来 pip 是直接把 urllib3 集成到了自己的包里面,不受系统安装包的影响。检查其中的 _version.py 里的版本信息,果然也是 1.26.x

出错的 pip 的版本是 20.3,把 pip 也降级到 20.2 以下,就没有问题了。

显然,鉴于 pip 的高频使用,这种致命的问题不可能没人报,所以在 pip 项目的 issue 列表里很快就找到了相关讨论:

https://github.com/pypa/pip/issues/9216

urllib3 更新了啥

根据 https://github.com/pypa/pip/issues/9216#issuecomment-741836058 所说,更改代理配置可以解决问题:

绕了好大一圈大概明白是怎么回事了:

以前 urllib3 其实并不支持 https 代理,也就是说代理服务器的地址虽然大家配置的是 https,但是一直都是悄无声息地就按照 http 连接的,刚好代理服务器确实也只支持 http,所以皆大欢喜。

现在 urllib3 要支持 https 代理了,那么既然配置代理是 https 就尝试用 https 的方式去连接,但是由于代理服务器其实只支持 http,所以没法处理请求,ssl 握手阶段就出错了。

注意,这里的 https 是指代理服务器自己的,和我们要访问的目标网站无关。

因为目标网站的协议和代理服务器的协议并不要求一样,所以只需要更改代理配置 ,将访问 https 网站的代理服务器地址改为 http 即可,也就是这样:

HTTPS_PROXY=http://proxy_ip:proxy_port

前面的 HTTPS_ 表示,如果访问的站点是 https 的,需要走这里配置的代理服务器;后面的 http:// 则表示这个代理服务器自己只支持 http

而我们一直以来看到的配置建议,这两者前后通常都是保持一致的:

HTTP_PROXY=http://proxy_ip:proxy_port
HTTPS_PROXY=https://proxy_ip:proxy_port

这个是错误的!

代理到底该咋配

Windows 10 中的代理服务器设置如下,并没有区分什么 httphttps:

手动给 requests 传入代理配置

requests 的请求参数中是支持指定代理服务器的,刚开始的代码没有指定:

url = 'https://github.com/'

r = requests.get(url)

前面在尝试解决问题的时候,也试过了传入代理服务器配置:


proxies={
'http': 'http://127.0.0.1:7890',
'https': 'https://127.0.0.1:7890'
} r = requests.get(url, proxies=proxies)

上面两种写法的效果其实是差不多一样的,结果当然也是一样出错。

按照上面 issue 中的修改建议改为:

proxies={
'http': 'http://127.0.0.1:7890',
'https': 'http://127.0.0.1:7890' # https -> http
} r = requests.get(url, proxies=proxies)

运行结果就 OK 了。

好了,现在我们可以不用降级版本了,但是却要多出一段配置,要改代码,总归还是不爽。

其实,如果是 Linux 系统是没这个问题的,本来代理配置就是通过环境变量 HTTP_PROXYHTTPS_PROXY 来设置的,改一下环境变量的值就可以了,麻烦还是在 Windows 系统中。

要搞明白 Python 代码是如何获取 Windows 系统中的代理服务器设置的。

谁解析的系统代理配置

在代码中不难发现,当用户有传入 proxies 参数时,requests 是通过标准库提供的 getproxies 函数来获取系统代理服务器配置的:


>>> # 如果是 python 2,则是 from urllib import getproxies
>>> from urllib.request import getproxies
>>> getproxies()
{'http': 'http://127.0.0.1:7890', 'https': 'https://127.0.0.1:7890', 'ftp': 'ftp://127.0.0.1:7890'}

上面显示的结果就是对应到截图中的代理配置。

注意,urlliburllib3 不是一个库,前者是 Python 标准库自带。

继续看代码:


elif os.name == 'nt': def getproxies():
"""Return a dictionary of scheme -> proxy server URL mappings. Returns settings gathered from the environment, if specified,
or the registry. """
return getproxies_environment() or getproxies_registry()

在 Windows 系统中,先从环境变量获取,如果没有则从注册表获取。

getproxies_environment 的逻辑比较简单,基本和 Linux 系统是一致的,就是环境变量配置成啥样就是啥样。这里我并没有配置环境变量,自然结果是空,最终的结果要看 getproxies_registry。按照其中的代码,从注册表中获取的配置如下:

代码里有两个处理逻辑:

                proxyServer = str(winreg.QueryValueEx(internetSettings,
'ProxyServer')[0])
if '=' in proxyServer:
# Per-protocol settings
for p in proxyServer.split(';'):
protocol, address = p.split('=', 1)
# See if address has a type:// prefix
if not re.match('(?:[^/:]+)://', address):
address = '%s://%s' % (protocol, address)
proxies[protocol] = address
else:
# Use one setting for all protocols
if proxyServer[:5] == 'http:':
proxies['http'] = proxyServer
else:
proxies['http'] = 'http://%s' % proxyServer
proxies['https'] = 'https://%s' % proxyServer
proxies['ftp'] = 'ftp://%s' % proxyServer

其中第一种是「每种协议各自配置」,下面第二种情况是「所有协议一个配置」。

在第二种情况中,如果带了 http: 就只配置 http 协议,否则(也就是我们现在的场景),针对同一个 proxyServer,添加 3 种协议。

在 Windows 中如何配置代理

第一种方法:通过环境变量设置

结果:


>>> getproxies()
{'https': 'http://127.0.0.1:7890', 'http': 'http://127.0.0.1:7890'}

一旦设置了环境变量,程序就直接从环境变量获取,系统的配置也就失效了。

第二种方法:按照上面的代码倒推出来系统配置

其中的地址框里的内容是:

http=http://127.0.0.1:1080;https=http://127.0.0.1

其中最后的端口只对最后面的那个地址有效,分号前面的地址需要加上端口。

对应的注册表中的值是:

显然这种方式有点诡异和麻烦,目前也没看到有相关的说明,不确定是否会影响其它程序的判断。

如此看来,还是第一种方法比较靠谱,就是不能利用系统配置有点遗憾。

我的疑惑

那么,在用户只给出了代理服务器的 IP 和 端口的情况下,原有的处理逻辑是不是错误,我也不敢断言。

回到最初犯错的地方:

HTTP_PROXY=http://proxy_ip:proxy_port
HTTPS_PROXY=https://proxy_ip:proxy_port

我现在还有一个疑惑点是,真的会存在一个代理服务器,能够在同一个端口同时支持 httphttps 么?如果是那样的话,为啥平常的 web 服务器还要有 80 和 443 两个端口对应不同的服务。

反过来,如果一个端口不可以同时支持两个协议的话,那么上面的配置的错误则更加明显,处理逻辑也就很有问题。

你对此有什么看法和理解,欢迎在评论区讨论!

总结

通过解决代理服务器错误,对 Python 是如何处理代理服务器配置有了更深入的了解。

Python 遭遇 ProxyError 问题记录的更多相关文章

  1. Python全栈开发记录_第一篇(循环练习及杂碎的知识点)

    Python全栈开发记录只为记录全栈开发学习过程中一些难和重要的知识点,还有问题及课后题目,以供自己和他人共同查看.(该篇代码行数大约:300行) 知识点1:优先级:not>and 短路原则:a ...

  2. Python开发之日志记录模块:logging

    1 引言 最近在开发一个应用软件,为方便调试和后期维护,在代码中添加了日志,用的是Python内置的logging模块,看了许多博主的博文,颇有所得.不得不说,有许多博主大牛总结得确实很好.似乎我再写 ...

  3. # Python 3 & 爬虫一些记录

    目录 Python 3 & 爬虫一些记录 交互模式和命令行模式 函数积累 语法积累 列表和元组 输入 交互模式下输入多行 爬虫 HTTP报文请求头User-Agent信息 解析库pyquery ...

  4. "利用python进行数据分析"学习记录01

    "利用python进行数据分析"学习记录 --day01 08/02 与书相关的资料在 http://github.com/wesm/pydata-book pandas 的2名字 ...

  5. python核心编程学习记录之基础知识

    虽然对python的基础知识有所了解,但是为了更深入的学习,要对python的各种经典书籍进行学习 第一章介绍python的优缺点,略过 第二章介绍python起步,第三章介绍python基础,仅记录 ...

  6. 《灰帽Python-黑客和逆向工程师的Python编程》学习记录

    ctypes是Python语言的一个外部库,提供和C语言兼容的数据类型,可以很方便的调用C DLL中的函数. 操作环境:CentOS6.5 Python版本:2.66 ctypes是强大的,强大到本书 ...

  7. python常用绘图软件包记录

    在没有使用python之前,觉得matlab的绘图功能还算可以~但现在发现python的绘图包真的好强大,绘制出的图像非常专业漂亮,但具体使用还有待学习,这里记录学习过程中遇到的python绘图包,以 ...

  8. Python全栈开发记录_第八篇(模块收尾工作 json & pickle & shelve & xml)

    由于上一篇篇幅较大,留下的这一点内容就想在这里说一下,顺便有个小练习给大家一起玩玩,首先来学习json 和 pickle. 之前我们学习过用eval内置方法可以将一个字符串转成python对象,不过, ...

  9. python添加fluent日志记录-aop

    python添加fluent日志,aop实现 1.配置fluent相关信息 fluent_config.ini fluent_config.ini [fluent.aop] #is support f ...

随机推荐

  1. Java Mybatis快速入门之基本使用

    目录 搭建环境 编写 Mybatis 核心配置文件 pom导出资源失败 测试 搭建环境 新建Maven项目 导入Maven依赖 <dependencies> <!--mysql驱动- ...

  2. CTF实验吧-WEB题目解题笔记(1)简单的登陆题

    1.简单的登陆题 解题链接: http://ctf5.shiyanbar.com/web/jiandan/index.php  Burp抓包解密 乱码,更换思路.尝试id intruder 似乎也没什 ...

  3. List使用Stream流进行集合Collection的各种运算汇总:对BigDecimal求和,某个字段的和、最大值、最小值、平均值,字段去重,过滤等

    写Java接口的朋友都知道,Java 8的更新,经常会用到过滤 list<Object> 里的数据,本文就对List使用Stream流进行集合Collection的各种运算做一个汇总! 优 ...

  4. 关于Mysql数据库建库字符集utf8mb4下,排序规则utf8mb4_bin和utf8mb4_general_ci选择造成的查询匹配大小写问题

    场景描述: 项目采用了分库模式进行不同业务的开发,在共有的功能模块进行设计的时候采用主从库,或者各分库之中存在同样的库表结构,在使用过程中做库表同步的时候一定要保证库表所在的数据库的字符集和编码格式是 ...

  5. 转 1 认识开源性能测试工具jmeter

    1 认识开源性能测试工具jmeter   典型的性能测试工具主要有2个,Load Runner和jmeter.Load Runner是商业化的,Jmeter是开源的.下面我们认识一下开源性能测试工具j ...

  6. XV6学习(2)Lab syscall

    实验的代码放在了Github上. 第二个实验是Lab: system calls. 这个实验主要就是自己实现几个简单的系统调用并添加到XV6中. XV6系统调用 添加系统调用主要有以下几步: 在use ...

  7. Java并发包源码学习系列:阻塞队列实现之PriorityBlockingQueue源码解析

    目录 PriorityBlockingQueue概述 类图结构及重要字段 什么是二叉堆 堆的基本操作 向上调整void up(int u) 向下调整void down(int u) 构造器 扩容方法t ...

  8. Go RPC 框架 KiteX 性能优化实践 原创 基础架构团队 字节跳动技术团队 2021-01-18

    Go RPC 框架 KiteX 性能优化实践 原创 基础架构团队 字节跳动技术团队 2021-01-18

  9. var、let、const之间的区别

    var声明变量可以重复声明,而let不可以重复声明var是不受限于块级的,而let是受限于块级var会与window相映射(会挂一个属性),而let不与window相映射var可以在声明的上面访问变量 ...

  10. Webpack4.0各个击破(7)plugin篇

    目录 一. plugin概述 1.1 Plugin的作用 1.2 Compiler 1.3 Compilation 二. 如何写一个plugin 四. 实战 [参考] 一. plugin概述 1.1 ...