在Roda这种轻量级Ruby Web框架中,路由块通常直接返回字符串或哈希来表示响应。一旦业务层抛出异常,默认行为往往是500 Internal Server Error。但实际项目里,我们需要区分资源不存在、无权限、参数校验失败等情况。如果在每个路由块里反复写 rescue 判断,代码会迅速膨胀。

好在Roda提供了集中式的 error_handler 插件,配合一个携带状态码的异常体系,就可以让异常类型直接决定HTTP响应码。这篇文章会围绕 Roda::Plugins::ErrorHandler::Code 这一思路,展示如何在实际项目中落地。
1. 理解 Roda::Plugins::ErrorHandler::Code 的定位
Roda 的 error_handler 插件提供了一个集中捕获异常的入口。当路由执行过程中抛出任何未被内部 rescue 的异常时,插件会调用配置块进行处理。Roda::Plugins::ErrorHandler::Code 这个名字可以理解为一类携带 HTTP 状态码的异常对象。它并不直接处理请求,而是让你的自定义异常具备“状态码”属性,从而被 error_handler 识别并映射为响应状态。
实际开发中,建议定义一个 AppError 基类,让它继承自 Roda::Plugins::ErrorHandler::Code(或混入其模块)。然后在基类中声明 status 方法,返回默认的 500。各个具体业务异常只需要继承 AppError 并覆写 status,就能在抛出时自动带上对应的状态码。这样异常体系和HTTP协议层就建立了直接联系。
需要注意的是,如果 Roda 版本中没有直接暴露这个常量,你可以根据同一思路自己实现一个等价模块:定义一个带 status 的异常基类即可。核心思想是把“什么错误”和“返回什么状态码”放在同一个对象里。
2. 配置 error_handler 插件并处理状态码
在 Roda 应用中启用插件非常简单:
class App < Roda
plugin :error_handler do |e|
status = e.respond_to?(:status) ? e.status : 500
response.status = status
{ error: e.message, status: status }.to_json
end
end
上面的配置块中,e 就是被捕获的异常对象。我们先判断它是否实现了 status 方法,如果有就取该值作为 HTTP 状态码,否则回退到 500。随后把状态码写入到 response.status,并返回 JSON 格式的错误信息。这样做可以让业务异常自动决定状态码,无需在每个路由中手动 rescue。
更贴近题目的做法,是让异常类直接继承或混入 Roda::Plugins::ErrorHandler::Code 提供的通用实现。这样 respond_to?(:status) 的判断可以省去,因为所有业务异常都保证有 status 方法。配置时可以直接调用 e.status。如果遇到未继承该基类的原生异常,仍然回退到500。
3. 定义不同异常类型与对应状态码
接下来定义三个常见异常类:
class AppError < StandardError
def status
500
end
end
class NotFoundError < AppError
def status
404
end
end
class UnauthorizedError < AppError
def status
401
end
end
class ValidationError < AppError
def status
422
end
end
这里没有直接使用 Roda::Plugins::ErrorHandler::Code 作为基类,而是模拟了它的核心机制:基类 AppError 提供默认状态码,子类通过覆写 status 方法返回各自对应的码。如果你确认 Roda 内部或团队封装中已有 Roda::Plugins::ErrorHandler::Code,可以将 AppError 的继承改为它,效果是一样的。重点是让每个异常类型明确知道自己的 HTTP 状态码。
在路由中使用这些异常时,代码会非常直观:
route do |r|
r.on "users" do
r.get do
user = User.find(params[:id])
raise NotFoundError, "用户不存在" unless user
user.to_json
end
end
end
一旦 User.find 返回 nil,路由块就会抛出 NotFoundError。error_handler 捕获后读取 status,接口就会返回 404,而不是默认的 500。这样做让错误语义更清晰,客户端可以依据状态码做后续处理。
4. 未映射异常的兜底和日志记录
并非所有异常都应该被客户端看到详细信息。对于没有定义 status 的 StandardError,我们应当统一返回 500,并在服务端记录完整堆栈。可以通过在 error_handler 块中加入日志逻辑来实现。
plugin :error_handler do |e|
status = e.respond_to?(:status) ? e.status : 500
if status == 500
logger.error "Unhandled error: #{e.class} - #{e.message}"
logger.error e.backtrace.join("\n")
end
response.status = status
{ error: e.message }.to_json
end
上述代码中,当 status 等于 500 时,我们记录完整的错误信息和堆栈;如果是业务异常(如 404、422),可以选择只记录简要信息或不记录。这样可以避免日志被大量客户端错误淹没。
还可以在响应体中增加 error_code 字段,方便前端程序做精确匹配。例如返回 { error: e.message, status: status } 之外再加一个 type: e.class.name。但要注意避免把敏感的内部类名暴露给客户端,可以在业务异常中定义 error_code 方法。
5. 测试异常到状态码的映射
为了验证配置是否生效,可以编写 RSpec 请求测试:
describe App do
include Rack::Test::Methods
def app
App
end
it "returns 404 for NotFoundError" do
allow(User).to receive(:find).and_return(nil)
get "/users/1"
expect(last_response.status).to eq(404)
expect(JSON.parse(last_response.body)["error"]).to eq("用户不存在")
end
end
测试可以确保异常映射逻辑稳定,不会因为路由重构而失效。如果某个异常的状态码被意外修改,测试会立即失败,从而保证API契约的一致性。
这类异常驱动状态码的方案非常适合团队协作。业务开发人员只需要抛出语义明确的异常,不需要关心HTTP细节;接口层又通过 error_handler 统一把异常转化为客户端可理解的状态码和错误信息。两者职责清晰,维护成本也明显下降。
Roda异常处理ErrorHandler插件HTTP状态码修改时间:2026-08-23 18:27:52