ssl / asyncio: rastrear incluso cuando se maneja el error

Intentando descargar y procesar archivos JPEG de las URL. Mi problema no es que la verificación del certificado falle para algunas URL, ya que estas URL son antiguas y es posible que ya no sean confiables, pero que cuando lo try...except... SSLCertVerificationError , todavía recibo el rastreo.

Sistema: Linux 4.17.14-arch1-1-ARCH, python 3.7.0-3, aiohttp 3.3.2

Ejemplo mínimo:

 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/'])) 

Salida:

 SSL handshake failed on verifying the certificate protocol:  transport: <_SelectorSocketTransport fd=6 read=polling write=> 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:  transport: <_SelectorSocketTransport closing fd=6 read=idle write=> 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 

El rastreo es generado por la implementación de asyncio del protocolo SSL, que invoca el controlador de excepciones del bucle de eventos. A través de un laberinto de interacciones entre el transporte y la interfaz de transmisión, ocurre que esta excepción se registra por el bucle de eventos y se propaga al usuario de API. La forma en que sucede es la siguiente:

  • Se produce una excepción durante el protocolo de enlace SSL.
  • SSLProtocol._on_handshake_complete recibe SSLProtocol._on_handshake_complete no-None y lo trata como un “error fatal” (en el contexto de handshake), es decir, invoca self._fatal_error y devuelve.
  • _fatal_error llama al controlador de excepciones del bucle de eventos para registrar el error. El controlador normalmente se invoca para las excepciones que se producen en las devoluciones de llamada en cola donde ya no hay un llamador para propagarlas, por lo que solo registra el rastreo de un error estándar para garantizar que la excepción no pase de forma silenciosa. Sin embargo…
  • _fatal_error pasa a llamar a transport._force_close , que llama a connection_lost nuevo en el protocolo.
  • La implementación connection_lost del protocolo del lector de secuencias establece la excepción como el resultado del futuro del lector de secuencias, propagándola así a los usuarios de la API de secuencias que la esperan.

No es obvio si es un error o una característica que el bucle de eventos registra la misma excepción y se pasa a connection_lost . Puede ser una solución alternativa para que BaseProtocol.connection_lost se defina como no-op , por lo que el registro adicional garantiza que un protocolo que simplemente hereda de BaseProtocol no silencia las posibles excepciones sensibles que se producen durante el protocolo de enlace SSL. Cualquiera que sea la razón, el comportamiento actual conduce al problema que experimenta el OP: capturar la excepción no es suficiente para suprimirla, aún se registrará un rastreo.

Para SSLCertVerificationError el problema, uno puede establecer temporalmente el controlador de excepciones en uno que no informe 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) 

Al agregar with suppress_ssl_exception_report() alrededor del código en fetch_url suprime el fetch_url no deseado.

Lo anterior funciona, pero se siente como una solución para un problema subyacente y no como el uso correcto de la API, por lo que presenté un informe de error en el rastreador.

Por razones desconocidas (¿error?) Aiohttp imprime la salida de error en la consola incluso antes de que se produzca una excepción. Se puede evitar la salida de error de redireccionamiento temporal con 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') # ... 

PD : Creo que puedes usar un tipo de excepción más común para detectar errores de clientes, por ejemplo:

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