在用Faraday调用第三方接口时,最容易踩坑的环节之一就是参数编码。明明本地拼好的Hash看起来没问题,发出去之后服务端却解析出了完全不同的结构,尤其是当参数里带数组或嵌套哈希时,问题会集中爆发。这篇文章就来系统梳理Faraday::Request::UrlEncoded这个中间件的工作机制,看看它是如何把Ruby对象转换成application/x-www-form-urlencoded格式的,以及遇到数组参数时该怎么正确处理。

UrlEncoded中间件的基本工作原理
先看中间件本身。Faraday::Request::UrlEncoded的作用很简单:当你通过req.body或conn.post(url, hash)传入一个Hash作为请求体时,它会把这个Hash序列化成URL编码后的键值对字符串,并把请求头设置为application/x-www-form-urlencoded。这个过程发生在请求发送之前,属于典型的请求拦截处理。
它的核心逻辑大致可以概括为两步:第一步判断body是不是Hash类型,如果不是就直接放行,交给其他中间件处理;第二步如果确实是Hash,就调用Faraday::Utils的序列化方法把Hash展开成字符串。下面这段代码展示了最基础的使用方式:
require 'faraday'
conn = Faraday.new(url: 'https://ipipp.com') do |f|
f.request :url_encoded # 启用表单编码中间件
f.adapter :net_http
end
# 最简单的键值对提交
response = conn.post('/api/login') do |req|
req.body = { username: 'rubyist', password: 'secret123' }
end
# 实际发送的body是 username=rubyist&password=secret123注意一个细节:这个中间件只处理body里的Hash,不会碰URL上的查询参数。查询字符串的编码由params选项和params_encoder负责,两者虽然共享一套编码约定,但属于不同的处理链路。理解这一点对后面排查数组参数问题很关键。
数组与嵌套结构的编码规则
真正让开发者困惑的地方在于数组参数。Faraday默认采用Rack风格的嵌套编码,也就是用方括号表达层级关系。一个{ids: [1, 2, 3]}会被编码成ids[]=1&ids[]=2&ids[]=3,一个嵌套哈希{user: {name: 'tom', age: 18}}则变成user[name]=tom&user[age]=18。这套约定在Rails、Sinatra等服务端框架里能被正确还原,但如果对端是Java的Spring或者某些第三方网关,它们可能只认ids=1&ids=2&ids=3这种重复键的写法,方括号写法反而会解析失败。
# 默认的嵌套编码
body = { ids: [1, 2, 3], user: { name: 'tom' } }
# 编码结果: ids[]=1&ids[]=2&ids[]=3&user[name]=tom
# 如果服务端要求重复键形式,需要指定FlatParamsEncoder
conn = Faraday.new do |f|
f.request :url_encoded
f.options.params_encoder = Faraday::FlatParamsEncoder
f.adapter :net_http
end另外还有一个经典坑:当数组本身作为查询参数放在URL上时,即使body部分用了UrlEncoded中间件,URL的编码也要看params_encoder的配置。曾有开发者在某个连接上设置了FlatParamsEncoder却发现新问题依旧,排查半天才发现是另外新建连接时忘了带上同样的配置。建议把encoder配置统一封装到构建连接的方法里,避免出现多套连接行为不一致的情况。
用自定义params_encoder完全接管序列化
当默认编码和FlatParamsEncoder都无法满足需求时,比如某些接口要求ids[0]=1&ids[1]=2这种带下标的形式,就需要自己实现encoder。自定义encoder只需要两个模块方法:encode(params)负责把Hash转成字符串,decode(query)负责反向解析。实现后通过f.options.params_encoder = YourEncoder挂载即可生效,body和URL查询参数都会走这套逻辑。
require 'faraday'
require 'cgi'
class IndexedParamsEncoder
module_function
# 把 ids: [1,2,3] 编码成 ids[0]=1&ids[1]=2&ids[2]=3
def encode(params)
return params.to_s if params.is_a?(String)
parts = []
traverse = lambda do |hash, prefix|
hash.each do |key, value|
full_key = prefix.empty? ? key.to_s : "#{prefix}[#{key}]"
if value.is_a?(Hash)
traverse.call(value, full_key)
elsif value.is_a?(Array)
value.each_with_index { |v, i| parts << "#{full_key}[#{i}]=#{escape(v)}" }
else
parts << "#{full_key}=#{escape(value)}"
end
end
end
traverse.call(params, '')
parts.join('&')
end
def decode(query)
Faraday::Utils.parse_query(query)
end
def escape(value)
CGI.escape(value.to_s)
end
end
conn = Faraday.new('https://ipipp.com') do |f|
f.request :url_encoded
f.options.params_encoder = IndexedParamsEncoder
f.adapter :net_http
end写自定义encoder时有几点经验值得注意。一是务必对值做URL转义,中文、空格和特殊字符如果不转义,轻则服务端解析异常,重则直接触发400错误;二是要处理好nil值,可以跳过或者编码成空字符串,具体看接口约定;三是decoder部分如果客户端不要求解析复杂查询串,可以简单复用Faraday自带的解析工具,没必要重复造轮子。有了这套机制,无论对接的接口用哪种编码风格,都能在Faraday这一层统一消化掉,业务代码里依然只管传干净的Ruby对象。
FaradayUrlEncoded数组参数修改时间:2026-09-16 10:40:50