ssl/asyncio:即使处理了错误也能回溯
ssl/asyncio: traceback even when error is handled
正在尝试从 URL 下载和处理 jpeg。我的问题不是某些 URL 的证书验证失败,因为这些 URL 很旧并且可能不再值得信赖,而是当我 try...except...
SSLCertVerificationError
时,我仍然得到回溯。
系统:
Linux 4.17.14-arch1-1-ARCH,python 3.7.0-3,aiohttp 3.3.2
最小示例:
import asyncio
import aiohttp
from ssl import SSLCertVerificationError
async def fetch_url(url, client):
try:
async with client.get(url) as resp:
print(resp.status)
print(await resp.read())
except SSLCertVerificationError as e:
print('Error handled')
async def main(urls):
tasks = []
async with aiohttp.ClientSession(loop=loop) as client:
for url in urls:
task = asyncio.ensure_future(fetch_url(url, client))
tasks.append(task)
return await asyncio.gather(*tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(['https://images.photos.com/']))
输出:
SSL handshake failed on verifying the certificate
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
File "/usr/lib/python3.7/asyncio/sslproto.py", line 625, in _on_handshake_complete
raise handshake_exc
File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
self._sslobj.do_handshake()
File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
SSL error in data received
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport closing fd=6 read=idle write=<idle, bufsize=0>>
Traceback (most recent call last):
File "/usr/lib/python3.7/asyncio/sslproto.py", line 526, in data_received
ssldata, appdata = self._sslpipe.feed_ssldata(data)
File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
self._sslobj.do_handshake()
File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
Error handled
由于未知原因(错误?)aiohttp 甚至在抛出任何异常之前就将错误输出打印到控制台。您可以使用 contextlib.redirect_stderr:
避免临时重定向错误输出
import asyncio
import aiohttp
from ssl import SSLCertVerificationError
import os
from contextlib import redirect_stderr
async def fetch_url(url, client):
try:
f = open(os.devnull, 'w')
with redirect_stderr(f): # ignore any error output inside context
async with client.get(url) as resp:
print(resp.status)
print(await resp.read())
except SSLCertVerificationError as e:
print('Error handled')
# ...
P.S. 我认为你可以使用更常见的异常类型来捕获客户端 errors,例如:
except aiohttp.ClientConnectionError as e:
print('Error handled')
回溯是由 asyncio 的 SSL 协议实现生成的,它调用了事件循环的 exception handler。通过 transport/protocol 和流层之间的交互迷宫,此异常由事件循环 和 记录并传播给 API 用户。发生的方式如下:
- SSL 握手过程中发生异常。
SSLProtocol._on_handshake_complete
接收非 None handshake_exc
并将其视为 "fatal error" (在握手上下文中),即调用 self._fatal_error
和 returns.
_fatal_error
调用事件循环的异常处理程序来记录错误。通常调用处理程序来处理队列回调中发生的异常,其中不再有调用者将它们传播到,因此它只是将回溯记录到标准错误以确保异常不会静默传递。然而...
_fatal_error
继续调用 transport._force_close
,后者根据协议调用 connection_lost
。
- 流 reader 协议的
connection_lost
implementation 将异常设置为流 reader 未来的结果,从而将其传播给流的用户API等待它。
事件循环记录并传递给 connection_lost
的同一异常是错误还是功能并不明显。这可能是 BaseProtocol.connection_lost
为 defined a no-op 的解决方法,因此额外的日志可确保仅从 BaseProtocol
继承的协议不会消除 SSL 握手期间发生的可能敏感的异常。不管是什么原因,当前的行为导致了 OP 遇到的问题:捕获异常不足以抑制它,回溯仍将被记录。
要解决此问题,可以暂时将异常处理程序设置为不报告的异常处理程序 SSLCertVerificationError
:
@contextlib.contextmanager
def suppress_ssl_exception_report():
loop = asyncio.get_event_loop()
old_handler = loop.get_exception_handler()
old_handler_fn = old_handler or lambda _loop, ctx: loop.default_exception_handler(ctx)
def ignore_exc(_loop, ctx):
exc = ctx.get('exception')
if isinstance(exc, SSLCertVerificationError):
return
old_handler_fn(loop, ctx)
loop.set_exception_handler(ignore_exc)
try:
yield
finally:
loop.set_exception_handler(old_handler)
在 fetch_url
中的代码周围添加 with suppress_ssl_exception_report()
可以抑制不需要的回溯。
上面的方法有效,但它强烈感觉像是潜在问题的解决方法,而不是正确的 API 用法,所以我在跟踪器中提交了 bug report。 编辑:问题同时得到解决,问题中的代码不再打印虚假回溯。
正在尝试从 URL 下载和处理 jpeg。我的问题不是某些 URL 的证书验证失败,因为这些 URL 很旧并且可能不再值得信赖,而是当我 try...except...
SSLCertVerificationError
时,我仍然得到回溯。
系统: Linux 4.17.14-arch1-1-ARCH,python 3.7.0-3,aiohttp 3.3.2
最小示例:
import asyncio
import aiohttp
from ssl import SSLCertVerificationError
async def fetch_url(url, client):
try:
async with client.get(url) as resp:
print(resp.status)
print(await resp.read())
except SSLCertVerificationError as e:
print('Error handled')
async def main(urls):
tasks = []
async with aiohttp.ClientSession(loop=loop) as client:
for url in urls:
task = asyncio.ensure_future(fetch_url(url, client))
tasks.append(task)
return await asyncio.gather(*tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(['https://images.photos.com/']))
输出:
SSL handshake failed on verifying the certificate
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
File "/usr/lib/python3.7/asyncio/sslproto.py", line 625, in _on_handshake_complete
raise handshake_exc
File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
self._sslobj.do_handshake()
File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
SSL error in data received
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport closing fd=6 read=idle write=<idle, bufsize=0>>
Traceback (most recent call last):
File "/usr/lib/python3.7/asyncio/sslproto.py", line 526, in data_received
ssldata, appdata = self._sslpipe.feed_ssldata(data)
File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
self._sslobj.do_handshake()
File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
Error handled
由于未知原因(错误?)aiohttp 甚至在抛出任何异常之前就将错误输出打印到控制台。您可以使用 contextlib.redirect_stderr:
避免临时重定向错误输出import asyncio
import aiohttp
from ssl import SSLCertVerificationError
import os
from contextlib import redirect_stderr
async def fetch_url(url, client):
try:
f = open(os.devnull, 'w')
with redirect_stderr(f): # ignore any error output inside context
async with client.get(url) as resp:
print(resp.status)
print(await resp.read())
except SSLCertVerificationError as e:
print('Error handled')
# ...
P.S. 我认为你可以使用更常见的异常类型来捕获客户端 errors,例如:
except aiohttp.ClientConnectionError as e:
print('Error handled')
回溯是由 asyncio 的 SSL 协议实现生成的,它调用了事件循环的 exception handler。通过 transport/protocol 和流层之间的交互迷宫,此异常由事件循环 和 记录并传播给 API 用户。发生的方式如下:
- SSL 握手过程中发生异常。
SSLProtocol._on_handshake_complete
接收非 Nonehandshake_exc
并将其视为 "fatal error" (在握手上下文中),即调用self._fatal_error
和 returns._fatal_error
调用事件循环的异常处理程序来记录错误。通常调用处理程序来处理队列回调中发生的异常,其中不再有调用者将它们传播到,因此它只是将回溯记录到标准错误以确保异常不会静默传递。然而..._fatal_error
继续调用transport._force_close
,后者根据协议调用connection_lost
。- 流 reader 协议的
connection_lost
implementation 将异常设置为流 reader 未来的结果,从而将其传播给流的用户API等待它。
事件循环记录并传递给 connection_lost
的同一异常是错误还是功能并不明显。这可能是 BaseProtocol.connection_lost
为 defined a no-op 的解决方法,因此额外的日志可确保仅从 BaseProtocol
继承的协议不会消除 SSL 握手期间发生的可能敏感的异常。不管是什么原因,当前的行为导致了 OP 遇到的问题:捕获异常不足以抑制它,回溯仍将被记录。
要解决此问题,可以暂时将异常处理程序设置为不报告的异常处理程序 SSLCertVerificationError
:
@contextlib.contextmanager
def suppress_ssl_exception_report():
loop = asyncio.get_event_loop()
old_handler = loop.get_exception_handler()
old_handler_fn = old_handler or lambda _loop, ctx: loop.default_exception_handler(ctx)
def ignore_exc(_loop, ctx):
exc = ctx.get('exception')
if isinstance(exc, SSLCertVerificationError):
return
old_handler_fn(loop, ctx)
loop.set_exception_handler(ignore_exc)
try:
yield
finally:
loop.set_exception_handler(old_handler)
在 fetch_url
中的代码周围添加 with suppress_ssl_exception_report()
可以抑制不需要的回溯。
上面的方法有效,但它强烈感觉像是潜在问题的解决方法,而不是正确的 API 用法,所以我在跟踪器中提交了 bug report。 编辑:问题同时得到解决,问题中的代码不再打印虚假回溯。