PAYATHON 2026

Rails 使用 HTTParty 携带 JSON 重定向到外部 URL

支付小周

结论

HTTP 重定向无法让浏览器跳转到外部 URL 时,携带 Rails 指定的自定义请求头和 JSON payload。

redirect_to 只会向浏览器返回 3xx 响应和 Location 地址。接下来的请求由浏览器重新发起,Rails 无法向其中注入 X-API-ID、X-API-Signature 等 headers,也无法指定新的 JSON body。

一般有三种处理方式:

  1. Rails 使用 HTTParty 在服务端调用外部 API,取得 checkout URL 后,再将浏览器重定向到该地址。
  2. Rails 充当代理,将外部服务返回的内容直接响应给浏览器。
  3. 前端使用 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 服务端完成,普通的浏览器重定向无法实现。

备注:内容仅供参考。