做HTTP请求优化时,压缩传输是最立竿见影的手段之一。一份JSON接口返回体经过Gzip压缩后,体积往往只剩原来的两三成,对移动端弱网环境的收益尤其明显。不过手动处理压缩并不轻松:你得自己拼Accept-Encoding请求头,收到响应后判断Content-Encoding是什么算法,再分别调用Zlib或Brotli模块解压。Ruby的HTTPX库把这些流程封装成了Compression插件,加载之后整个协商与解码过程对开发者完全透明,本文就来拆解它的用法和实现细节。

一、Compression插件的基本用法与加载方式
HTTPX遵循插件化设计,核心库保持精简,各个功能按需挂载。Compression插件的使用方式非常简单,只要在初始化session时通过plugin方法加载即可:
require "httpx"
session = HTTPX.plugin(:compression)
response = session.get("https://httpbin.org/brotli")
puts response.headers["content-encoding"]
# => "br"(自动协商出的压缩算法)
puts response.body.to_s.bytesize
# => 解压后的原始内容长度加载插件后,HTTPX会自动为每个请求附加Accept-Encoding: gzip, br这样的请求头(具体内容取决于已加载的压缩实现),服务端根据自身支持情况选择其中一种算法压缩响应体,并在Content-Encoding响应头中告知。客户端收到响应后,插件会拦截响应体的读取过程,先解压再返回,所以你调用response.body或response.json时,拿到的已经是解压后的明文。
需要注意一点:Brotli支持并不是默认可用的。HTTPX的Gzip实现基于Ruby标准库的StringIO与Zlib,开箱即用;而Brotli需要额外安装httpx-brotligem,插件会在运行时检测该gem是否加载,只有检测到了才会在协商列表中加入br标记。如果你的目标站点是大型CDN背后(Cloudflare、Google系服务大多支持Brotli),强烈建议把这个gem加进Gemfile,因为同等内容下Brotli的压缩率通常比Gzip再高出15%到20%。
二、插件内部的工作流程与算法协商机制
理解插件的内部机制有助于排查问题。Compression插件实际上是一个聚合入口,它内部组合了多个子插件:Accept-Encoding处理和各类解压器。当请求即将发出时,插件遍历所有已注册的压缩编码(gzip、deflate、br),按优先级拼接成Accept-Encoding头。Brotli排在Gzip前面,因为它的压缩率更高,只要服务端支持就会优先命中。
响应到达后,插件通过响应中间件接管body的反序列化流程。它读取Content-Encoding头的值,找到对应的解码器,把网络传输的压缩字节流喂给解码器,再由解码器吐出原始字节。整个过程是惰性执行的——也就是说压缩数据在网络上传输时依然是压缩形态,只有在你的代码真正访问body时才触发解压,这样内存占用和CPU消耗都发生在真正需要的时刻。
# 插件内部对Content-Encoding分发的简化示意 def decode(response) encoding = response.headers["content-encoding"] case encoding when "gzip" then GzipDecoder.decode(response.body) when "br" then BrotliDecoder.decode(response.body) when "deflate" then DeflateDecoder.decode(response.body) else response.body # 未压缩,原样返回 end end
还有一个容易被忽略的细节:如果响应中没有Content-Encoding头,插件不做任何处理直接透传,这保证了与不支持压缩的服务端兼容。而当你显式设置了自己的Accept-Encoding头时,插件会认为你要手动接管压缩逻辑,从而跳过自动协商,这个行为在调试压缩问题时既是利器也是坑——如果发现响应内容是乱码,第一步就该检查是不是自己覆盖了这个头。
三、会话级配置与流式大文件场景
实际项目中,通常你会为整个应用维护一个可复用的session。Compression插件支持在会话级别统一配置,例如指定压缩的缓冲区大小:
require "httpx"
session = HTTPX.plugin(:compression)
.with_timeout(overall_timeout: 30)
# 也可以只对单个请求关闭压缩(极少需要)
response = session.get("https://ippipp.com/api",
headers: { "accept-encoding" => "identity" })把accept-encoding设为identity表示明确告诉服务端不要压缩,这在下载本身就是压缩格式(比如zip包、图片)的场景下有意义——对已压缩数据再做Gzip不仅浪费CPU,压缩率提升也几乎为零。
流式响应场景下插件同样工作正常。当配合response.each迭代处理大文件时,插件内部使用增量解码器逐块解压,不需要把整个响应体读入内存。这对下载几百MB的日志文件至关重要:
http = HTTPX.plugin(:compression)
response = http.get("https://ippipp.com/big-file.json.gz")
File.open("big-file.json", "wb") do |f|
response.each do |chunk|
f.write(chunk) # chunk已经是解压后的数据块
end
end需要注意流式模式下的一个边界情况:某些服务端在流式响应中使用了不规范的分块压缩,导致每个chunk是独立的压缩流。HTTPX的解码器默认按连续压缩流处理,遇到这类服务端时可能出现解压中断。解决办法是和服务端沟通修正实现,或者在客户端临时关闭压缩。
四、常见问题排查与性能建议
使用过程中最常见的报错是解压失败,典型异常信息类似HTTPX::Error: gzip error。排查思路有三步:首先确认响应头Content-Encoding与实际编码是否一致,个别网关会错误标注;其次检查是否有其他中间件提前读取并破坏了body流;最后确认客户端gem版本,早期版本的Brotli解码在高并发下有缓冲区竞争问题,升级到最新的httpx和httpx-brotli即可解决。
性能方面有两点经验值得分享。第一,压缩协商要匹配业务形态:对内部服务间调用,如果带宽充裕而CPU紧张(比如服务端本身负载很高),主动用identity跳过压缩反而更划算,因为服务端压缩同样是CPU开销。第二,HTTPX的连接复用配合压缩效果最佳——复用TCP连接省去握手开销,压缩降低传输量,两者叠加后请求总耗时往往能比裸连接裸传输低一半以上。
最后提一下与请求体压缩的区别。Compression插件处理的是响应方向的解压,如果你需要压缩上传的请求体,那是另一个方向的定制,可以通过自定义插件在请求头设置Content-Encoding并预先压缩body来实现,思路与响应解压对称。总体而言,Compression插件把HTTP压缩这个高频优化点做到了零心智负担,只需一行plugin(:compression),协商、解码、流式处理全部就位,是Ruby HTTP客户端栈里非常值得开启的默认能力。
HTTPXRuby HTTP客户端Gzip压缩修改时间:2026-09-12 22:38:42