ssl/asyncio:即使处理错误也会追溯

dea*_*nha 11 python ssl openssl python-asyncio aiohttp

尝试从URL下载和处理jpeg.我的问题是不适合的网址有些证书验证失败,因为这些网址都是老可能不再是值得信赖的,但是,当我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/']))
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

use*_*342 14

追溯由asyncio的SSL协议实现生成,该协议调用事件循环的异常处理程序。通过传输和流接口之间的交互迷宫,碰巧该异常既被事件循环记录,又传播到API用户。发生的方式如下:

  • SSL握手期间发生异常。
  • SSLProtocol._on_handshake_complete接收non-None handshake_exc并将其视为“致命错误”(在握手上下文中),即调用self._fatal_error并返回。
  • _fatal_error调用事件循环的异常处理程序以记录错误。通常,对于在队列回调中发生的异常(通常不再有调用者将其传播到该异常)调用处理程序,因此它仅将回溯记录为标准错误,以确保异常不会静默传递。然而...
  • _fatal_error继续调用transport._force_close,该connection_lost协议回叫该协议。
  • 流读取器协议的connection_lost 实现将异常设置为流读取器未来的结果,从而将其传播给等待它的流API用户。

如果是bug或功能,事件循环记录并传递到的相同异常是不明显的connection_lost。将BaseProtocol.connection_lost定义为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)
Run Code Online (Sandbox Code Playgroud)

with suppress_ssl_exception_report()周围添加代码fetch_url可抑制不必要的回溯。

上面的方法可以工作,但是强烈感觉像是解决潜在问题的方法,而不是正确使用API​​,因此我在跟踪器中提交了错误报告

  • 错误报告[已记录](https://bugs.python.org/issue34506)至错误跟踪器。 (2认同)