Python asyncio搭配websockets 8.1时Future异常未检索处理问题
异步WebSocket开发异常捕获异常:
Future exception was never retrieved 根因与解决方案 基础开发环境
- Python 3.6.1 运行环境
- 依赖版本:websockets==8.1
- 现有实现逻辑:基于asyncio事件循环封装
TestConnection类,完成WebSocket连接建立、异步消息接收全流程;在run协程中通过asyncio.gather(..., return_exceptions=True)配置任务异常收集,按照文档预期,该配置会自动消费协程抛出的所有异常,无需额外捕获。
问题现象与对照测试结果
核心异常表现
- 触发场景:主动断开网络时,底层抛出
websockets.exceptions.ConnectionClosedError(错误码1006,代表连接内部异常关闭、无明确关闭原因) - 矛盾表现:
- 控制台可以正常打印gather收集到的上述ConnectionClosedError异常结果,说明gather本身对外部业务任务的异常捕获是正常的
- 控制台仍持续弹出
Future exception was never retrieved的未检索Future异常告警
反常对照测试
将原本负责接收处理消息的on_data方法修改为:异步休眠5秒后直接抛出RuntimeError("on_data error raised")。完成修改后重新测试,上述未检索Future异常的提示会完全消失。
问题根因拆解
这一现象和大多数开发者对asyncio.gather的功能误解直接相关,并非gather配置失效:
- gather的异常捕获边界有明确限制
asyncio.gather(return_exceptions=True)仅会捕获、自动消费直接传入参数列表的最外层协程/任务抛出的异常;对于任务执行链路中,由第三方库内部自行创建、未被当前业务代码await、未绑定结果检索回调的游离后台Future,gather完全没有监控和消费权限。 - websockets 8.1版本的内部实现特性
该版本的WebSocket连接对象在初始化时,会把「底层TCP连接的读写状态监控逻辑」拆分为独立的内部Task挂到事件循环上,这部分Task不属于业务代码传给gather的接收任务范畴:- 当触发1006类无原因连接关闭时,这个内部监控Task会先抛出ConnectionClosedError,且该版本的实现没有给这个Task绑定任何异常检索回调,直接触发了asyncio的默认未检索异常告警
- 对照测试中告警消失的原因:手动在
on_data里抛RuntimeError的场景下,异常是从业务代码主动抛出的,websockets内部的连接关闭流程会走正常的优雅关闭分支,那个独立的内部监控Task会顺利执行完成、抛出可被正常消费的正常关闭异常,不会触发无回调的未检索异常分支。
- 为什么控制台能看到gather收集的异常?
业务侧传给gather的异步接收任务,同样会因为底层连接断开抛出ConnectionClosedError,这部分异常被gather正常捕获、打印,和websockets内部游离Task抛出的异常完全是两个独立的异常实例,所以才会出现“一边正常打印捕获到的异常,一边弹未检索告警”的矛盾现象。
可落地的解决建议
- 根源修复方案:websockets 8.1版本的这个内部异常未回收问题,已经在8.1.2及后续9.x稳定版本中被修复,升级到兼容Python 3.6的最新稳定补丁版本即可从根源消除告警。
- 无升级权限场景的临时规避:
- 为当前asyncio事件循环注册全局异常处理器,对预期内的1006连接关闭类异常做过滤,避免无意义的控制台刷屏;处理器逻辑中可以主动检索游离Task的异常,将预期内的异常标记为已消费,消除告警。
- 在连接关闭的收尾逻辑中,通过websockets连接对象暴露的内部接口,获取其关联的内部监控Task,为其添加异常检索回调,确保Task抛出的所有异常都能被正常消费。
- 开发注意点:不要将
return_exceptions=True作为全链路异常捕获的银弹,它的作用域仅覆盖gather直接管理的业务任务,第三方库内部生成的游离Task异常,必须依赖库自身的异常接口、或者全局事件循环异常回调做兜底处理。
内容的提问来源于stack exchange,提问作者David Co
相关产品推荐
相关产品推荐

