已捕获Mongoid::Errors::DocumentNotFound仍抛异常的问题排查
我在Rails控制器中设置了一个作为before_action执行的authenticate_user方法,代码如下:
def authenticate_user Knock::AuthToken.new(token: token).entity_for(User) rescue Mongoid::Errors::DocumentNotFound render nothing: true, status: 401 end
经调试确认rescue语句已触发(byebug断点命中),但Mongoid::Errors::DocumentNotFound异常仍会被抛出,报错信息如下:
Mongoid::Errors::DocumentNotFound ( message: Document(s) not found for class User with id(s) 1. summary: When calling User.find with an id or array of ids, each parameter must match a document in the database or this error will be raised. The search was for the id(s): 1 ... (1 total) and the following ids were not found: 1. resolution: Search for an id that is in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error when searching for a single id, or only the matched documents when searching for multiples.): app/controllers/api/base_controller.rb:12:in `authenticate_user'
我使用Ruby的rescue关键字多年从未遇到该问题,当前运行环境为Ruby 2.5、Rails 5.2、Mongoid 7.0.1。请问为何捕获异常后仍会抛出,该如何阻止异常抛出?
问题根源:Ruby语法解析导致rescue作用域错误
你遇到的问题核心是代码格式引发的语法解析歧义。当你把整个方法逻辑压缩到一行时,Ruby会按照运算符优先级解析代码,最终你的代码会被理解成:
def authenticate_user # rescue仅作用于这单个表达式,捕获异常后返回nil(默认值) Knock::AuthToken.new(token: token).entity_for(User) rescue Mongoid::Errors::DocumentNotFound # 不管前面是否触发异常,这行render都会执行,但异常的传播逻辑被打乱了 render nothing: true, status: 401 end
这种写法下,rescue只负责处理entity_for这一行的异常,但后续的代码执行逻辑并没有和异常捕获绑定,导致异常没有被完全终止传播——哪怕你用byebug命中了rescue,也可能因为语法解析的问题,让异常的处理流程出现偏差。
解决方案:修正代码格式,明确rescue作用域
解决这个问题的关键是拆分代码行,让rescue明确作为方法级别的异常处理逻辑,而非仅绑定单个表达式。修改后的代码如下:
def authenticate_user Knock::AuthToken.new(token: token).entity_for(User) rescue Mongoid::Errors::DocumentNotFound head :unauthorized end
这样写的话,rescue会完整捕获方法中前面语句抛出的Mongoid::Errors::DocumentNotFound异常,执行head :unauthorized发送401响应,并且彻底终止异常的传播。
另外补充一点:Rails 5.2中render nothing: true已经被弃用,官方推荐使用head :unauthorized(更符合RESTful规范,代码也更简洁)。
额外验证点(如果问题仍存在)
- 确认
Mongoid::Errors::DocumentNotFound是StandardError的子类(Mongoid 7.0中确实如此,所以rescue可以正常捕获) - 检查Knock的
entity_for方法内部是否有重新抛出异常的逻辑(比如在内部rescue后又执行了raise),不过根据你提到的byebug断点信息,这个概率很低
按照上述代码修改后,异常应该会被正确捕获,不会再继续抛出了。
内容的提问来源于stack exchange,提问作者Ten Bitcomb

