Rails 使用 HTTParty 携带 JSON 重定向到外部 URL
结论
HTTP 重定向无法让浏览器跳转到外部 URL 时,携带 Rails 指定的自定义请求头和 JSON payload。
redirect_to 只会向浏览器返回 3xx 响应和 Location 地址。接下来的请求由浏览器重新发起,Rails 无法向其中注入 X-API-ID、X-API-Signature 等 headers,也无法指定新的 JSON body。
一般有三种处理方式:
- Rails 使用 HTTParty 在服务端调用外部 API,取得 checkout URL 后,再将浏览器重定向到该地址。
- Rails 充当代理,将外部服务返回的内容直接响应给浏览器。
- 前端使用
fetch发送 JSON 和 headers,但外部服务必须满足相应的 CORS 要求。
推荐方案:服务端提交,再重定向用户
如果外部 API 接收 JSON 后会返回支付页或处理页地址,可以先让 Rails 完成 API 请求,再重定向浏览器:
def checkout
response = HTTParty.post(
"https://some-test.supertester.com/checkout",
verify: true,
body: JSON.parse(@payment).to_json,
headers: {
"Content-Type" => "application/json",
"Accept" => "application/json",
"X-TimeStamp" => Time.current.to_i.to_s,
"X-API-ID" => @api_id.to_s,
"X-API-Signature" => @api_signature.to_s
},
timeout: 20
)
unless response.success?
Rails.logger.error(
"Checkout request failed: status=#{response.code}"
)
render plain: "无法创建外部处理请求", status: :bad_gateway
return
end
data = JSON.parse(response.body)
checkout_url = data.fetch("checkout_url")
redirect_to checkout_url, allow_other_host: true
rescue JSON::ParserError, KeyError => e
Rails.logger.error("Invalid checkout response: #{e.message}")
render plain: "外部服务返回了无效响应", status: :bad_gateway
rescue Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error("Checkout request timed out: #{e.message}")
render plain: "外部服务响应超时", status: :gateway_timeout
end
以上代码假设外部 API 返回的数据格式如下:
{
"checkout_url": "https://some-test.supertester.com/checkout/session/abc123"
}
较新的 Rails 版本在重定向到其他域名时,可能需要设置 allow_other_host: true。是否需要该参数,取决于项目使用的 Rails 版本和安全配置。
如果 HTTParty 已自动解析 JSON,也可以这样读取:
checkout_url = response.parsed_response.fetch("checkout_url")
如果外部响应就是最终页面
如果外部接口不返回跳转地址,而是直接返回 HTML、JSON 或其他结果,可以让 Rails 将响应转发给浏览器:
def checkout
response = HTTParty.post(
"https://some-test.supertester.com/checkout",
verify: true,
body: JSON.parse(@payment).to_json,
headers: {
"Content-Type" => "application/json",
"X-TimeStamp" => Time.current.to_i.to_s,
"X-API-ID" => @api_id.to_s,
"X-API-Signature" => @api_signature.to_s
},
timeout: 20
)
render(
body: response.body,
status: response.code,
content_type: response.headers["content-type"]
)
end
这种做法相当于让 Rails 充当服务端代理,浏览器地址栏中的 URL 不会改变。转发响应头时也要进行筛选,不应直接复制 Set-Cookie、Location 等所有 headers。
前端直接发送请求
如果必须由浏览器直接请求外部站点,可以使用 JavaScript:
const response = await fetch(
"https://some-test.supertester.com/checkout",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-TimeStamp": String(Math.floor(Date.now() / 1000)),
"X-API-ID": apiId,
"X-API-Signature": apiSignature
},
body: JSON.stringify(payment)
}
);
const result = await response.json();
if (result.checkout_url) {
window.location.assign(result.checkout_url);
}
这种方式有两个重要限制:
- 外部服务必须正确配置 CORS,并允许使用这些自定义 headers。
- API ID、签名密钥和其他敏感凭据不应暴露在浏览器代码中。
因此,只要请求涉及签名或私密凭据,一般仍应由 Rails 服务端发送。
为什么 head :no_content 没有作用
head :no_content 只会让 Rails 返回一个没有正文的 HTTP 响应:
head :no_content
它能避免 Rails 查找同名模板,但不会让浏览器访问外部地址。同样,下面的代码只是将结果返回给当前请求,并不是重定向:
render json: response.parsed_response
Controller action 必须明确执行一种响应操作,例如重定向:
redirect_to checkout_url, allow_other_host: true
或者返回 JSON:
render json: response.parsed_response
如果没有明确响应,Rails 会按照默认规则查找与 action 同名的模板。
不能依赖 307 或 308 重定向解决
307 Temporary Redirect 和 308 Permanent Redirect 可以要求客户端保留原请求的方法和 body,但无法让 Rails 将其替换为另一套 JSON payload,也不能可靠地给跨域后的请求添加自定义 headers。
例如:
redirect_to external_url, status: :temporary_redirect, allow_other_host: true
这种写法最多只能保留用户发送给 Rails 的原始请求内容,无法将 HTTParty 请求中的 headers 和 body 附加到跳转请求上,因此不适合当前需求。
注意事项
示例中的 header 名称末尾不能带冒号。正确写法是:
"X-API-ID" => @api_id.to_s,
"X-API-Signature" => @api_signature.to_s
不要写成:
"X-API-ID:" => @api_id.to_s,
"X-API-Signature:" => @api_signature.to_s
生产环境也不应使用:
verify: false
该设置会关闭 TLS 证书验证,使 HTTPS 请求失去必要的身份校验。除非需要在受控测试环境中排查证书问题,否则应保持验证开启,并正确配置可信证书。
具体实现取决于外部服务的协议。如果对方提供创建会话并返回页面 URL 的 API,应采用 Rails 服务端 POST,再通过 redirect_to 跳转到返回地址的流程。如果对方只接受一次包含 JSON 和签名 headers 的 POST,该请求就必须由 Rails 服务端完成,普通的浏览器重定向无法实现。
备注:内容仅供参考。
