Ruby项目里使用Faraday发HTTP请求时,如果接口返回4xx或5xx错误,很多时候我们希望直接拿到一个明确的异常对象,而不是手动判断response.status再分支处理。Faraday自带的RaiseError中间件就是干这件事的,它内部维护了一张默认的状态码与异常类的映射表。这张表怎么定义、包含哪些状态码、异常类之间是什么继承关系,是本文要展开讲清楚的内容。

RaiseError中间件的默认状态码映射表
Faraday的RaiseError中间件位于Faraday::Response::RaiseError,它并没有把状态码映射硬编码在on_complete方法里,而是抽取成了一个模块级常量,便于查阅和扩展。这个常量在Faraday的早期版本中叫STATUS_CODES,后来整理为Error::ERRORS,本质上是一个哈希:键是整数形式的状态码,值是对应的异常类。
下面这张表的内容大致如下(以Faraday 1.x主线为参考,不同小版本可能有细微差异):
module Faraday
class Error
ERRORS = {
400 => Faraday::BadRequestError,
401 => Faraday::UnauthorizedError,
403 => Faraday::ForbiddenError,
404 => Faraday::ResourceNotFound,
407 => Faraday::ProxyAuthError,
408 => Faraday::RequestTimeoutError,
409 => Faraday::ConflictError,
410 => Faraday::GoneError,
412 => Faraday::PreconditionFailedError,
415 => Faraday::UnsupportedMediaTypeError,
416 => Faraday::RangeNotSatisfiableError,
422 => Faraday::UnprocessableEntityError,
423 => Faraday::LockedError,
429 => Faraday::TooManyRequestsError,
500 => Faraday::InternalServerError,
501 => Faraday::NotImplementedError,
502 => Faraday::BadGatewayError,
503 => Faraday::ServiceUnavailableError,
504 => Faraday::GatewayTimeoutError
}.freeze
end
end
可以看到,映射表并没有覆盖所有状态码,比如402、405、406就没有单独的异常类。当响应状态码不在表里时,中间件不会抛任何异常,响应对象会原样返回,由调用方自行处理。这个设计是刻意的:只有语义上明确需要业务关注的错误才映射成异常,避免把正常的业务流程淹没在大量的rescue分支里。
中间件的on_complete钩子会在响应完成时被调用,逻辑非常简单,查表然后raise:
def on_complete(env) status = env.status key = Error::ERRORS[status] raise key.new(response_values(env)) if key end
注意raise发生在on_complete里,而不是请求发起前,这意味着连接层面的错误(如DNS解析失败、超时)走的是Faraday::ConnectionFailed和Faraday::TimeoutError,与状态码异常是两套体系,不要混淆。
异常类的继承体系与通用处理
上面列出的这些具体异常类,并不是各自独立的顶层类,它们全部继承自Faraday::Error,而Faraday 1.x开始Faraday::Error又继承自Ruby标准库的StandardError。这意味着你可以用一个rescue捕获所有Faraday抛出的错误:
begin
conn = Faraday.new(url: 'https://api.eastauto.cn') do |f|
f.response :raise_error
end
response = conn.get('/users/1')
rescue Faraday::ResourceNotFound => e
puts "资源不存在: #{e.response[:status]}"
rescue Faraday::UnauthorizedError => e
puts "鉴权失败,请检查token"
rescue Faraday::Error => e
puts "其他Faraday错误: #{e.class} #{e.message}"
end
这些异常类本身定义得非常薄,多数只是空类体,存在的意义就是提供可区分的类型标识。同时它们还保留了4xx与5xx的粗粒度分组:ClientError和ServerError是两个中间层的父类,所有4开头的客户端错误继承前者,5开头的服务端错误继承后者。如果你只关心错误是客户端还是服务端造成的,直接rescue这两个父类就够了,不必逐个枚举具体异常。
另一个实用细节是异常对象上的response方法。raise时传入的response_values会被保存下来,你可以通过e.response拿到一个包含:status、:headers、:body的哈希。很多接口会把错误详情写在响应体里,捕获异常后解析e.response[:body]往往能拿到更具体的错误信息,比如后端返回的错误码和提示文案,这一点在排查线上问题时特别有用。
需要提醒的是,老版本Faraday(0.x)里的类名是Faraday::Error::ClientError这种嵌套写法,1.0之后为了兼容Ruby的关键字参数和更清晰的命名空间,改成了顶层的Faraday::ClientError。如果你的项目还在用0.x版本,写rescue时要注意类名差异,升级时最好全局搜一遍替换。
自定义状态码映射与扩展异常类
默认表满足不了所有场景。比如你调用的第三方接口把业务错误统一放在200响应的body里,或者某个内部服务约定418表示特殊业务态,这时候就需要扩展映射。最直接的方式是定义自己的异常类并塞进ERRORS哈希。由于哈希被freeze了,不能直接修改,但可以先dup再合并:
class MyApi::TeapotError < Faraday::ClientError; end class MyApi::PaymentRequiredError < Faraday::ClientError; end Faraday::Error::ERRORS.merge( 402 => MyApi::PaymentRequiredError, 418 => MyApi::TeapotError )
这里有个Ruby知识点:Hash#merge会返回新哈希,不修改原对象,所以即使原表被freeze也不会抛FrozenError。但要注意,中间件在on_complete里读取的是Error::ERRORS这个常量本身,单纯merge返回新对象并赋给一个局部变量是不生效的。正确做法要么在Faraday加载早期、常量还没freeze前介入(不推荐,侵入性强),要么干脆不用官方中间件,自己写一个response中间件,维护一张完全可控的映射表:
class MyErrorMiddleware < Faraday::Middleware
CUSTOM_ERRORS = {
402 => MyApi::PaymentRequiredError,
418 => MyApi::TeapotError
}.freeze
def on_complete(env)
klass = Faraday::Error::ERRORS[env.status] || CUSTOM_ERRORS[env.status]
raise klass.new(response_values(env)) if klass
end
private
def response_values(env)
{ status: env.status, headers: env.response_headers, body: env.body }
end
end
conn = Faraday.new(url: 'https://api.eastauto.cn') do |f|
f.use MyErrorMiddleware
end
自定义中间件的好处是不动Faraday内部结构,升级Faraday版本时不会因为常量结构变化而翻车,同时优先级策略(先查官方表还是先查自定义表)也完全由你掌控。
还有一种常见诉求是对200但body里标记了业务错误的响应抛异常,比如接口返回{"code": 40001, "message": "余额不足"}。这同样可以在自定义中间件里处理:判断env.status == 200后解析body,根据业务码抛出自定义的BusinessError。把HTTP语义错误和业务语义错误统一收敛到同一个异常处理链路里,上层代码会干净很多。
最后总结一下要点:默认映射表是冻结的哈希常量,覆盖了主流的4xx和5xx状态码;所有异常继承自Faraday::Error,并按客户端、服务端分成两大类;扩展时优先考虑自定义中间件而不是修改官方常量;捕获异常后别忘了e.response里藏着完整的响应上下文,它是排查问题的关键线索。掌握这些,Faraday的错误处理基本就不会再有盲区了。