Rails 7 生产环境 GET 请求缺少模板且无法重定向
结论
问题不在于 production 环境无法处理 GET 请求,而在于 paye action 执行后没有完成响应。
当前代码只会在以下条件成立时重定向:
@payment_response['status'] == 1
如果支付接口返回失败、status 是字符串 "1",或者第二次请求因重复使用同一个 txnid 被拒绝,代码就会跳过 redirect_to,直接运行到方法末尾。Rails 随后执行默认渲染,尝试查找 orders/paye.html.erb,于是出现:
OrdersController#paye is missing a template for request formats: text/html
对于“第一次成功、第二次失败”的情况,应重点检查这一行:
"txnid" => "#{@order.id}"
同一个订单每次都使用相同的交易编号。支付网关通常要求每次发起支付时使用唯一的 txnid,具体规则仍应以 Easebuzz 当前接口文档和实际响应为准。
为什么 Rails 会查找模板
Controller action 必须通过以下任一方式结束请求:
redirect_torenderhead- 发送文件或流式响应
当前代码只有成功分支:
if @payment_response['status'] == 1
redirect_to(...)
end
这里没有失败分支。只要条件不成立,Rails 就会进行隐式渲染,相当于尝试:
render :paye
由于项目中没有 app/views/orders/paye.html.erb,Rails 因此报告模板缺失。
这条错误说明 action 已经通过路由进入并运行到了末尾,并不表示 Easebuzz::Payment.initiate 没有执行。
先记录支付接口的真实响应
不要在 Passenger 部署中依赖 puts。它会把内容写入进程的标准输出,而这些输出可能由 Apache、Passenger 或 systemd 接管,不一定会进入 Rails 的 production 日志。
应改用 Rails logger:
Rails.logger.info(
"Easebuzz initiation result: status=#{@payment_response['status'].inspect}"
)
诊断期间也可以记录过滤后的响应:
Rails.logger.info(
"Easebuzz initiation response: #{@payment_response.except('data').inspect}"
)
不要将密钥、支付令牌、完整手机号或其他敏感数据写入日志。data 很可能包含不适合长期保存的支付信息。
另外,redirect_to 不会立即终止 Ruby 方法。最好显式使用 return,让控制流更清楚:
return redirect_to(...)
补全成功和失败分支
可以先将 action 调整为下面的结构:
def paye
txnid = "#{@order.id}-#{SecureRandom.uuid}"
payment_response = Easebuzz::Payment.initiate(
{
"txnid" => txnid,
"amount" => amount.to_f,
"firstname" => current_user.name,
"email" => current_user.email,
"phone" => current_user.phone_number,
"productinfo" => "Payment for Order #{@order.id}",
"surl" => successE_order_url(@order, protocol: "https"),
"furl" => failedTransaction_order_url(@order, protocol: "https")
}
)
Rails.logger.info(
"Easebuzz initiation: order_id=#{@order.id} " \
"txnid=#{txnid} status=#{payment_response['status'].inspect}"
)
if payment_response["status"].to_s == "1" && payment_response["data"].present?
return redirect_to(
"https://pay.easebuzz.in/pay/#{payment_response['data']}",
allow_other_host: true,
status: :see_other
)
end
Rails.logger.error(
"Easebuzz initiation failed: order_id=#{@order.id} " \
"response=#{payment_response.except('data').inspect}"
)
redirect_to order_path(@order),
alert: "无法发起支付,请稍后重试。"
rescue StandardError => e
Rails.logger.error(
"Easebuzz initiation exception: order_id=#{@order&.id} " \
"#{e.class}: #{e.message}"
)
redirect_to order_path(@order),
alert: "支付服务暂时不可用,请稍后重试。"
end
这段代码做了以下调整:
- 每次支付尝试都生成唯一的
txnid。 - 同时兼容接口返回整数
1和字符串"1"。 - 成功后显式执行
return redirect_to。 - 失败时同样执行
redirect_to,避免触发隐式模板渲染。 - 使用
Rails.logger记录执行结果。 - 回调地址通过 URL helper 生成,减少手工拼接出错的可能。
如果业务要求将支付重试关联到订单,建议单独建立支付记录,保存 txnid、订单、支付状态和网关响应之间的关系,而不是只使用 order.id。
检查 status 的实际类型
开发和生产环境可能使用不同的网关、SDK 配置或接口响应,因此不能假设 status 一定是整数。
例如:
1 == 1
# true
"1" == 1
# false
原条件可能在一个环境中成立,在另一个环境中却不成立:
if @payment_response["status"] == 1
诊断时可以记录字段的值和类型:
Rails.logger.info(
"Easebuzz status value=#{@payment_response['status'].inspect}, " \
"class=#{@payment_response['status'].class}"
)
确认 Easebuzz 的响应格式后,再决定是否使用:
payment_response["status"].to_s == "1"
重定向前也不应只检查状态字段,还需要确认 data 存在:
payment_response["status"].to_s == "1" &&
payment_response["data"].present?
支付发起操作应使用 POST
发起支付会创建交易或改变业务状态,不适合使用 GET。GET 请求可能被浏览器预加载、缓存、爬虫访问,也可能因用户刷新页面而重复发起。
路由可以改为:
resources :orders do
member do
post :paye
post :successE
post :failedTransaction
end
end
页面上使用提交按钮:
<%= button_to "立即支付",
paye_order_path(order),
method: :post,
data: { turbo: false } %>
Easebuzz 的成功和失败回调究竟应配置为 GET 还是 POST,取决于网关实际使用的回调方式。如果网关通过 POST 回调,就必须提供 POST 路由;如果浏览器通过 GET 返回,则应配置 GET 路由。最终应以真实回调请求和 Easebuzz 文档为准。
将 paye 改为 POST 并不能自动解决模板错误。只要 action 运行到末尾时没有执行 render、redirect_to 或 head,Rails 仍会查找模板。
检查 production 回调地址
生产环境应优先使用 HTTPS:
"surl" => successE_order_url(@order, protocol: "https"),
"furl" => failedTransaction_order_url(@order, protocol: "https")
也可以在 production 配置中设置默认主机:
# config/environments/production.rb
Rails.application.routes.default_url_options = {
host: "example.in",
protocol: "https"
}
之后直接使用:
successE_order_url(@order)
failedTransaction_order_url(@order)
还需要确认:
- 回调路由确实存在。
- action 名称与路由完全一致。
- 域名没有使用示例中的括号。
- HTTPS 证书有效,且公网可以访问回调地址。
- production 使用了正确的 Easebuzz key、salt 和生产接口。
- Web 服务器没有阻止相应的 HTTP 方法。
Ruby 方法名区分大小写,建议统一使用小写 snake_case,例如:
def paye
end
def success_e
end
def failed_transaction
end
对应路由也应保持一致,避免混用 payE、paye、successE 等名称。
最可能的完整故障链
结合“第一次成功、第二次开始报模板缺失”这一现象,最可能的过程是:
- 第一次请求使用
order.id作为txnid,支付网关接受了请求。 - 接口返回成功状态,代码执行
redirect_to。 - 第二次请求仍然使用相同的
txnid。 - 网关返回失败状态,或者返回了不同格式的状态。
if @payment_response['status'] == 1条件不成立。- action 没有失败分支,继续运行到末尾。
- Rails 尝试渲染
orders/paye.html.erb。 - 模板不存在,因此报错。
需要同时修正唯一交易编号、响应类型判断、失败分支和日志记录方式。添加一个空的 paye.html.erb 只会掩盖真正的支付失败,并不能解决问题。
备注:内容仅供参考。
