PAYATHON 2026

如何向 Third Party Payment API 返回 HTTP Status 200

支付老李

结论

Response.StatusCode = 200 本身没有问题。第三方仍然重试,通常不是状态码写法有误,而是它没有在 60 秒内收到完整的 HTTP 响应,或者响应被异常、重定向、认证模块、IIS、反向代理等改成了其他状态。

通知接口适合采用“可靠接收后,再异步处理”的方式:

  1. 快速校验通知来源和签名。
  2. 将原始通知持久化到数据库或消息队列。
  3. 立即返回第三方要求的确认响应。
  4. 在后台更新订单并处理其他业务。
  5. 使用第三方交易号实现幂等,允许第三方重复发送同一通知。

如果业务处理超过了第三方设置的 60 秒 timeout,即使最后设置了 StatusCode = 200,第三方仍会重试。

为什么设置了 200 仍会重试

ASP.NET 在没有异常时,默认响应通常就是 200 OK。下面这段判断多数情况下不会改变实际行为:

If Response.StatusCode <> HttpStatusCode.OK Then
    Response.StatusCode = HttpStatusCode.OK
End If

真正需要检查的是整个响应过程。

响应时间超过 60 秒

如果数据库更新、其他 API 调用、邮件发送或日志记录等操作耗时超过 60 秒,第三方会先判定 timeout 并安排重试。服务器即使在第 70 秒返回 200 OK,也已经晚了。

返回 200 之后又发生异常

代码设置 200 后,如果后续代码出现未处理异常,ASP.NET 或 IIS 最终可能返回 500 Internal Server Error。第三方收到的是最终响应,不是代码执行过程中设置过的状态码。

请求被重定向

认证、HTTPS 跳转、URL 规范化或应用路由可能返回:

  • 301 Moved Permanently
  • 302 Found
  • 307 Temporary Redirect
  • 308 Permanent Redirect
  • 401 Unauthorized
  • 403 Forbidden

浏览器可能自动跟随重定向,所以人工访问时看起来正常。支付平台的通知程序未必会跟随,也可能直接把重定向视为失败。

IIS 或代理修改了响应

IIS、自定义错误页、负载均衡器、WAF、CDN 或反向代理都可能修改状态码、中断连接,或者在自身 timeout 后返回错误。

第三方要求特定响应正文

有些支付平台除了要求 HTTP 200,还会要求响应正文为 OK、SUCCESS 或指定的 JSON。题目中的描述只明确提到 HTTP 200,因此不能直接认定必须返回某个字符串,应以支付平台的通知协议为准。

推荐的处理流程

通知接口收到请求后,只做确认通知所必需的操作:

接收请求
  → 校验签名和必要字段
  → 持久化原始通知
  → 返回 HTTP 200
  → 后台处理订单

其中,持久化不能省略。如果通知还没有可靠保存,接口就直接返回 200,而应用随后崩溃,这条支付结果可能永久丢失。

数据库写入或消息入队成功后,才可以认为通知已经可靠接收。订单更新、库存处理和邮件发送等操作可以交给后台执行。

ASP.NET Framework / VB.NET 示例

如果 notification page 是 ASP.NET Web Forms 页面,可以参考下面的写法:

Imports System.Net
Imports System.Web

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    Try
        Dim rawPayload As String

        Using reader As New IO.StreamReader(Request.InputStream)
            rawPayload = reader.ReadToEnd()
        End Using

        ' 1. 校验签名、商户号及必要字段
        If Not IsValidNotification(Request, rawPayload) Then
            Response.Clear()
            Response.StatusCode = CInt(HttpStatusCode.BadRequest)
            Response.TrySkipIisCustomErrors = True
            Response.ContentType = "text/plain"
            Response.Write("INVALID")
            Context.ApplicationInstance.CompleteRequest()
            Return
        End If

        ' 2. 将通知可靠写入数据库或消息队列
        ' 应以第三方交易号或通知 ID 建立唯一约束,保证幂等
        SaveNotification(rawPayload)

        ' 3. 立即确认接收成功
        Response.Clear()
        Response.StatusCode = CInt(HttpStatusCode.OK)
        Response.TrySkipIisCustomErrors = True
        Response.ContentType = "text/plain"
        Response.Write("OK")

        Context.ApplicationInstance.CompleteRequest()
        Return

    Catch ex As Exception
        LogNotificationError(ex)

        Response.Clear()
        Response.StatusCode = CInt(HttpStatusCode.InternalServerError)
        Response.TrySkipIisCustomErrors = True
        Response.ContentType = "text/plain"
        Response.Write("ERROR")

        Context.ApplicationInstance.CompleteRequest()
        Return
    End Try
End Sub

Response.TrySkipIisCustomErrors = True 可以减少 IIS 使用自定义错误页替换应用响应的情况。

不建议依赖 Response.End(),因为它可能触发 ThreadAbortException。也不要使用 Response.Close() 强制断开连接,否则第三方可能把不完整的响应视为网络错误。使用 CompleteRequest() 并立即 Return,更容易避免后续代码继续执行。

如果支付平台明确规定成功响应不能包含正文,可以省略:

Response.Write("OK")

如果平台要求返回 SUCCESS 或特定 JSON,则必须严格按照协议输出。

ASP.NET Core 示例

在 ASP.NET Core 中,可以在通知可靠入库后直接返回:

[HttpPost("payment/notify")]
public async Task<IActionResult> Notify()
{
    using var reader = new StreamReader(Request.Body);
    var rawPayload = await reader.ReadToEndAsync();

    if (!IsValidNotification(Request, rawPayload))
    {
        return BadRequest("INVALID");
    }

    await SaveNotificationAsync(rawPayload);

    return Ok("OK");
}

如果成功响应不需要正文:

return StatusCode(StatusCodes.Status200OK);

邮件发送、复杂报表和长时间的外部 API 调用等非必要操作,不要放在返回响应之前执行。

必须实现幂等处理

即使响应完全正确,也不能假设通知只会到达一次。例如,第三方已经成功发送通知,但在读取响应时遇到网络中断,仍可能重发同一条通知。

可以使用以下字段之一作为幂等键:

  • 第三方 transaction ID
  • notification ID
  • 商户订单号与支付流水号的组合

数据库中最好建立唯一约束,不能只依赖先查询再插入。例如:

CREATE UNIQUE INDEX UX_PaymentNotification_TransactionId
ON PaymentNotification(TransactionId);

收到重复通知时,不要再次扣款、发货或增加余额。如果该交易已经处理成功,应直接返回第三方要求的成功响应。

如何确认实际返回了什么

不要只查看 notification page 的业务日志。还要检查服务器访问日志或反向代理日志,确认每次请求的:

  • 最终 HTTP status code
  • 请求开始和结束时间
  • 总耗时
  • 是否发生重定向
  • 是否出现 499、502、503、504 等代理错误
  • 第三方是否在第一次请求尚未结束时发起了下一次请求

也可以从外部网络使用相同的 HTTP 方法请求通知地址,例如:

curl -i -X POST https://example.com/payment/notify \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "transaction_id=test123&status=paid"

首先查看响应首行是否为:

HTTP/1.1 200 OK

还要确认响应在 60 秒内结束。普通浏览器打开 URL 时通常发送 GET,支付平台发送的却可能是 POST,因此浏览器测试不能代替真实的通知请求。

其他注意事项

  • return_url 来自持卡人的浏览器,不能作为判断支付成功的唯一依据。服务端 notify_url 通常更可靠。
  • 必须验证签名、金额、币种、商户号和订单号,不能只根据请求中的“支付成功”字段更新订单。
  • 不要为了阻止重试而对所有请求无条件返回 200,否则伪造通知或尚未保存成功的通知也会被确认。
  • 如果通知已经可靠保存,但后台业务处理失败,可以在内部重试,不必要求支付平台反复发送。
  • notification URL 不应跳转到登录页面,也不应依赖浏览器 Cookie、Session 或 CSRF 表单令牌。
  • 如果多次通知的间隔正好接近 60 秒,应先排查接口耗时和网络 timeout,不要继续修改 StatusCode 的赋值方式。

备注:内容仅供参考。