SSL/Asyncio:即使处理了错误,仍会出现回溯。

13

尝试从URL下载并处理jpeg。我的问题不是某些URL的证书验证失败,因为这些URL可能已经过时且不再可信,而是当我try...except... SSLCertVerificationError时,仍然会收到traceback。

系统: 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本身记录,而是由asyncio记录。在aiohttp方面无需做任何处理。 - Andrew Svetlov
2个回答

14

回溯是由asyncio实现的SSL协议生成的,该协议调用事件循环的异常处理程序。通过传输/协议和流层之间的一系列交互,此异常被事件循环记录并且传播给API用户。发生的方式如下:

  • SSL握手期间发生异常。
  • SSLProtocol._on_handshake_complete接收到非空的handshake_exc并将其视为“致命错误”(在握手上下文中),即调用self._fatal_error并返回。
  • _fatal_error调用事件循环的异常处理程序记录错误。该处理程序通常用于在排队的回调中发生异常,在这些回调中已经没有调用者来传播它们,因此它只是将跟踪记录到标准错误以确保异常不会悄悄地传递。但是...
  • _fatal_error继续调用transport._force_close,这会在协议上调用connection_lost
  • 流读取器协议的connection_lost实现将异常设置为流读取器的future结果,从而将其传播给等待它的流API用户。
这段文字涉及到IT技术,具体内容为:无法确定同一个异常既被事件循环记录,又被传递给connection_lost是bug还是feature。可能是为了解决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)

fetch_url代码周围添加with suppress_ssl_exception_report()可以抑制不需要的回溯。以上方法可行,但强烈感觉这是对潜在问题的解决方法而不是正确API使用,因此我在跟踪器中提交了bug report编辑:现在问题已经得到解决,问题代码不再打印错误回溯信息。

我明白了。感谢您提供这么详细的解释。我很感激能够更深入地了解事情在底层是如何运作的。 - deasmhumnha
我试过了,它似乎在我的实际函数中起作用。谢谢你。 - deasmhumnha
2
错误报告已经被记录到错误跟踪器中。点击此处查看详情。 - user4815162342
当处理asyncio.TimeoutError时,似乎这个解决方案有时会引发另一个异常,称ignore_exc缺少一个带有回溯到ignore_exc内部的old_handler_fn(ctx)行的参数ctx(因此出现了某种意外的递归)。 - deasmhumnha
需要注意的是,在我的程序中,我修改了您的示例以忽略更一般的 ssl.SSLError - deasmhumnha
1
@DezmondGoff 代码存在一个错误,当 suppress_ssl_exception_report 嵌套时会导致其无法正常工作。回答已经进行了编辑,现在应该已经修复了。 - user4815162342

1

由于未知原因(可能是bug),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')

# ...

附言: 我认为您可以使用更常见的异常类型来捕获客户端错误,例如:

except aiohttp.ClientConnectionError as e:
    print('Error handled')

你的解决方案在示例中有效,但在我的实际代码中无效。重定向块中的代码未被执行。 - deasmhumnha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接