PAYATHON 2026

如何为 Instant Payment Notification 返回 HTTP 200

支付小周

结论

在 Rails 控制器中,可以用 head :ok 返回只有 HTTP 状态码、没有响应正文的 200 OK:

def success
  # 处理 IPN 消息

  head :ok
end

不要只写:

render status: 200

如果没有指定响应内容,Rails 在某些版本或调用方式下仍可能查找与 action 对应的模板,进而触发 MissingTemplate。

推荐写法

现有 action 可以改成:

def success
  transaction = Transaction.find_by_merchant_reference(params[:external_ref])

  if transaction && transaction.status != "Completed"
    date_time   = params[:date_time]
    amount      = params[:amount]
    narrative   = params[:narrative]
    network_ref = params[:network_ref]
    external_ref = params[:external_ref]
    msisdn      = params[:msisdn]
    signature   = params[:signature]

    if verfiy_signiture(
      date_time,
      amount,
      narrative,
      network_ref,
      external_ref,
      msisdn
    ) == signature
      user = User.find(transaction.user_id)
      user.balance += transaction.amount * 0.975
      user.save!

      transaction.update!(status: "Completed")

      TopUp.account_topup(
        user.name,
        user.email,
        user.currency,
        transaction.amount,
        transaction.merchant_reference
      ).deliver
    end
  end

  head :ok
end

head :ok 生成的响应类似:

HTTP/1.1 200 OK
Content-Length: 0

这种响应适用于不需要返回 HTML、JSON 或跳转页面的 IPN webhook。

为什么会出现 MissingTemplate

Rails action 执行完毕后必须完成一次响应。如果没有明确调用 render、redirect_to 或 head,Rails 通常会尝试隐式渲染与 action 同名的模板,例如:

app/views/<controller_name>/success.html.erb

IPN 请求一般不需要页面模板,因此应该明确调用:

head :ok

如果需要返回简单文本,也可以写成:

render plain: "OK", status: :ok

如果对方只要求 HTTP 200,不要求响应正文,使用 head :ok 更直接。

不要使用路径辅助方法代替响应

原代码中的:

user_home_path(@current_user)

只会生成一个 URL 字符串,既不会执行跳转,也不会结束请求。如果确实需要让浏览器跳转,应使用:

redirect_to user_home_path(@current_user)

不过,IPN 是服务器之间的通知请求,通常不需要跳转。这里应删除该调用,并在最后执行 head :ok。

同样,flash[:notice] 主要用于下一次浏览器页面请求。它对 webhook/IPN 通常没有意义,也不能代替 HTTP 响应。

确保所有分支都返回 200

如果接口要求无论消息是否得到处理都返回 HTTP 200,应将 head :ok 放在正常执行路径的末尾,而不是只放在签名验证成功的分支里:

def success
  process_ipn_if_applicable
  head :ok
end

如果查询、保存或发送邮件时抛出异常,程序会提前退出,末尾的 head :ok 无法执行,Rails 通常会返回 500。如果协议要求任何处理结果都返回 200,就需要明确处理预期异常:

def success
  begin
    process_ipn
  rescue StandardError => e
    Rails.logger.error("IPN processing failed: #{e.class}: #{e.message}")
  end

  head :ok
end

不建议在不了解故障类型时长期吞掉所有异常。更稳妥的做法是只捕获可预期的异常,记录足够的信息,并将耗时或容易失败的业务处理交给后台任务。

还应注意

  • 验证签名后,再修改余额、更新交易状态或发送邮件。
  • IPN 可能重复发送,因此处理逻辑必须保持幂等。只检查状态可能产生并发竞争,关键更新最好放入数据库事务,并配合锁或唯一约束。
  • 应检查 user.save 和交易更新是否成功。使用 save!、update! 可以避免保存失败后继续执行。
  • 不要在日志中记录完整签名、密钥或其他敏感支付数据。
  • 如果该 endpoint 受 Rails CSRF 校验保护,而外部支付平台无法提供 Rails CSRF token,需要仅针对该 webhook action 配置适当的 CSRF 处理,同时继续使用 IPN 签名验证身份。

备注:内容仅供参考。