Python异常未按预期处理:WebSocket超时错误未写入日志
问题描述
用户编写了如下异步异常处理代码:
try: jobs1 = await data.get_jobs() jobs2 = await user.get_jobs() tasks = [asyncio.create_task(x) for x in [loop()] + jobs1 + jobs2] print(f"{len(tasks)} tasks") await asyncio.gather(*tasks) except Exception as e: e = str(e) [x.cancel() for x in tasks] if not any([x in e for x in IGNORE_ERRORS]): user.log({"error": f"{e} \n {traceback.format_exc()}"})
预期所有异常都通过user.log写入log.json,但有一个异常直接打印到控制台:
data transfer failed Traceback (most recent call last): File "D:\software\miniconda\lib\site-packages\websockets\legacy\protocol.py", line 959, in transfer_data message = await self.read_message() File "D:\software\miniconda\lib\site-packages\websockets\legacy\protocol.py", line 1029, in read_message frame = await self.read_data_frame(max_size=self.max_size) File "D:\software\miniconda\lib\site-packages\websockets\legacy\protocol.py", line 1104, in read_data_frame frame = await self.read_frame(max_size) File "D:\software\miniconda\lib\site-packages\websockets\legacy\protocol.py", line 1161, in read_frame frame = await Frame.read( File "D:\software\miniconda\lib\site-packages\websockets\legacy\framing.py", line 68, in read data = await reader(2) File "D:\software\miniconda\lib\asyncio\streams.py", line 707, in readexactly await self._wait_for_data('readexactly') File "D:\software\miniconda\lib\asyncio\streams.py", line 501, in _wait_for_data await self._waiter length = fut.result() File "D:\software\miniconda\lib\asyncio\windows_events.py", line 826, in _poll value = callback(transferred, key, ov) File "D:\software\miniconda\lib\asyncio\windows_events.py", line 494, in finish_recv return ov.getresult() OSError: [WinError 121] 信号灯超时时间已到
原因分析
异常触发于任务取消后的后续操作
当asyncio.gather因某任务抛出异常进入except块后,你调用[x.cancel() for x in tasks]取消所有任务。但websockets的连接协程(如transfer_data)在被取消时,内部的IO操作会抛出OSError: [WinError 121]。这个异常是在任务被取消后触发的,此时asyncio.gather的异常处理流程已经结束,无法捕获后续抛出的异常,导致asyncio将其直接打印到控制台。未处理被取消任务的残留异常
asyncio.create_task创建的任务被取消后,如果抛出额外异常且未被显式await或处理,asyncio会将这类未捕获的任务异常默认输出到控制台。你的代码在取消任务后,没有处理这些任务可能抛出的后续异常。代码执行顺序的逻辑漏洞
except块中先将异常转为字符串,再执行任务取消操作。此时取消任务触发的异常无法被当前except块捕获——当前块只负责处理await asyncio.gather(*tasks)抛出的初始异常,任务取消后的异常属于独立执行流,不在当前捕获范围内。
解决建议
方案1:处理取消任务后的异常
在取消任务后,显式await每个任务并处理可能的异常,避免未捕获的异常被打印:
import traceback import asyncio try: jobs1 = await data.get_jobs() jobs2 = await user.get_jobs() tasks = [asyncio.create_task(x) for x in [loop()] + jobs1 + jobs2] print(f"{len(tasks)} tasks") await asyncio.gather(*tasks) except Exception as e: e_str = str(e) # 取消所有任务 for task in tasks: task.cancel() # 处理取消任务后可能抛出的异常 for task in tasks: try: await task except (asyncio.CancelledError, Exception) as te: te_str = str(te) if not any(ignore in te_str for ignore in IGNORE_ERRORS): user.log({"error": f"Task cancelled error: {te_str} \n {traceback.format_exc()}"}) # 记录初始异常 if not any(ignore in e_str for ignore in IGNORE_ERRORS): user.log({"error": f"{e_str} \n {traceback.format_exc()}"})
方案2:使用return_exceptions=True统一捕获异常
给asyncio.gather添加return_exceptions=True参数,这样所有任务的异常会被收集为返回值,而非直接抛出,可统一处理所有异常:
import traceback import asyncio jobs1 = await data.get_jobs() jobs2 = await user.get_jobs() tasks = [asyncio.create_task(x) for x in [loop()] + jobs1 + jobs2] print(f"{len(tasks)} tasks") results = await asyncio.gather(*tasks, return_exceptions=True) # 遍历结果处理所有异常 for result in results: if isinstance(result, Exception): e_str = str(result) if not any(ignore in e_str for ignore in IGNORE_ERRORS): user.log({"error": f"{e_str} \n {traceback.format_exc()}"})
内容的提问来源于stack exchange,提问作者em1971

