重定向是HTTP协议里非常常见的一种服务端响应方式,服务端返回301、302等状态码,并在响应头的Location字段里告诉客户端新的地址。用Ruby写爬虫或者调用第三方接口时,如果不去处理这些跳转,拿到的往往只是一个空壳响应。Net::HTTP默认是不会自动跟随重定向的,需要开发者自己动手处理,而处理不当又可能陷入无限循环,本文就来把这件事讲透。

先弄清楚几种重定向状态码的区别
很多人一看到301、302就直接跟着跳,其实这两个状态码的语义差别不小。301表示永久重定向,按规范客户端后续请求应该直接使用新地址;302表示临时重定向,理论上客户端下次访问原地址时还是先访问原地址。更关键的是303、307和308这几个状态码:303明确要求客户端用GET方式访问新地址,307则要求保持原有的请求方法不变,308是301的保持方法版本。
这些区别在什么场景下会出问题呢?典型的情况是你提交一个POST表单,服务端处理后返回302跳转到结果页。按照早年浏览器的实际行为,几乎所有客户端库在遇到302时都把POST改成GET,这在大多数场景下没问题。但如果服务端返回的是307,你继续用GET去请求就会失败,因为307要求你原封不动地把POST方法和请求体带到新地址去。理解了这些,写重定向逻辑时才能对症下药。
下面这个表格总结了常见重定向状态码的处理策略:
| 状态码 | 含义 | 是否保持原请求方法 |
|---|---|---|
| 301 | 永久重定向 | 规范建议改为GET,浏览器实际行为多为改GET |
| 302 | 临时重定向 | 多数客户端改为GET |
| 303 | 参见其他 | 必须改为GET |
| 307 | 临时重定向 | 必须保持原方法 |
| 308 | 永久重定向 | 必须保持原方法 |
用Net::HTTP手动实现重定向跟随
Net::HTTP是Ruby标准库,不需要额外安装依赖,但它的自动重定向支持很弱。Ruby 2.x之后曾经出现过Net::HTTP.get_redirect这样的方法,不过在实际项目中,更通用的做法是自己写一个跟随循环。核心思路是:发起请求,检查响应码,如果在301、302、303、307、308之列,就从Location头取出新地址,拼接完整URL后再发一次请求。
这里有个容易踩的坑:Location头返回的可能是绝对地址,也可能是相对路径。比如响应头里写的是/login而不是https://www.ipipp.com/login,直接拿来请求就会失败。标准库里的uri提供了URI.join方法可以方便地把相对路径解析成绝对地址。另一个细节是,重定向过程中请求头里的Host、Cookie都要相应更新,尤其是登录态依赖Cookie的场景,不带上上一步返回的Set-Cookie,跳转后就会被踢回登录页。
下面是一段完整可运行的实现代码,包含最大跳转次数限制:
require 'net/http'
require 'uri'
MAX_REDIRECTS = 10
def fetch_with_redirects(url_str, limit = MAX_REDIRECTS)
# 超过最大跳转次数,抛出异常防止无限循环
raise ArgumentError, 'HTTP重定向次数过多,疑似无限循环' if limit.zero?
uri = URI.parse(url_str)
http = Net::HTTP.new(uri.host, uri.port)
# https站点需要开启SSL
http.use_ssl = (uri.scheme == 'https')
request = Net::HTTP::Get.new(uri.request_uri)
request['User-Agent'] = 'Mozilla/5.0 RubyClient'
response = http.request(request)
case response
when Net::HTTPRedirection
location = response['location']
# 相对路径转绝对路径
new_url = URI.join(uri.to_s, location).to_s
puts "重定向到: #{new_url},剩余次数: #{limit - 1}"
fetch_with_redirects(new_url, limit - 1)
else
response
end
end
resp = fetch_with_redirects('https://www.ipipp.com')
puts resp.code
puts resp.body[0, 200]
这段代码用了递归来实现循环跳转,写法简洁,但要注意递归深度受limit控制,不会造成栈溢出。如果你不喜欢递归,改成while循环也可以,逻辑完全等价:
def fetch_iteratively(url_str, limit = 10)
current = URI.parse(url_str)
jumps = 0
while jumps < limit
http = Net::HTTP.new(current.host, current.port)
http.use_ssl = (current.scheme == 'https')
response = http.request(Net::HTTP::Get.new(current.request_uri))
unless response.is_a?(Net::HTTPRedirection)
return response
end
current = URI.join(current.to_s, response['location'])
jumps += 1
end
raise '重定向次数超限'
end
防止无限循环的几种策略
无限循环是重定向处理里最危险的问题。服务端配置错误、两个地址互相跳转,或者带会话参数的地址每次都返回新的跳转,都会让客户端陷入死循环。最基本的防线就是前面代码里的最大跳转次数限制,浏览器一般定为20次,Ruby社区常见做法是10次左右,取多大看你业务需要,但没有上限是绝对不可接受的。
除了次数限制,还有一种更精细的检测手段:记录访问过的URL集合。如果新地址已经在访问历史里出现过,说明出现了循环跳转,可以立刻终止并输出诊断信息。这种方式比单纯数次数更快定位问题,尤其在排查服务端配置错误时特别有用:
def fetch_detect_loop(url_str)
visited = Set.new
current = URI.parse(url_str)
loop do
break if visited.include?(current.to_s)
visited << current.to_s
http = Net::HTTP.new(current.host, current.port)
http.use_ssl = (current.scheme == 'https')
response = http.request(Net::HTTP::Get.new(current.request_uri))
return response unless response.is_a?(Net::HTTPRedirection)
current = URI.join(current.to_s, response['location'])
end
raise "检测到循环重定向: #{current}"
end
另外还有两点工程实践值得注意。一是重定向循环检测要考虑规范化问题,同一个地址带上不同的查询参数顺序,字符串比较会认为不同,稳妥的做法是先对URL做规范化再比较。二是超时设置,http.open_timeout和http.read_timeout都应该设置合理值,否则即使不会无限循环,每次跳转都卡上几十秒,体验同样糟糕。
用第三方库简化重定向处理
如果项目允许引入gem,完全没必要手写这些逻辑。rest-client和HTTParty都内置了重定向跟随,配置简单。rest-client默认最多跟随10次重定向,还能通过回调拿到完整的重定向历史:
require 'rest-client'
response = RestClient.get('https://www.ipipp.com',
max_redirects: 5,
user_agent: 'Mozilla/5.0')
puts response.code
puts response.history.size # 经历了几次重定向
HTTParty的用法同样直观,它默认会自动处理重定向,想控制行为时可以传块精细定制,比如只在特定域名之间允许跳转,防止重定向被恶意利用跳到钓鱼站点:
require 'httparty'
class Client
include HTTParty
follow_redirects true
def self.safe_get(url)
get(url, follow_redirects: true) do |req|
req.options[:max_redirects] = 5
end
end
end
选择建议很简单:简单的脚本场景用标准库手写足够,还能完全掌控细节;正式项目里用成熟的gem,省去维护重定向边界逻辑的成本。无论哪种方式,最大跳转次数、超时时间、307等方法保持语义这三点都不能忽略,这才是写出健壮HTTP客户端的关键所在。
Ruby HTTP重定向Net::HTTP301跳转修改时间:2026-09-15 07:46:33