如何向 Third Party Payment API 返回 HTTP Status 200
结论
Response.StatusCode = 200 本身没有问题。第三方仍然重试,通常不是状态码写法有误,而是它没有在 60 秒内收到完整的 HTTP 响应,或者响应被异常、重定向、认证模块、IIS、反向代理等改成了其他状态。
通知接口适合采用“可靠接收后,再异步处理”的方式:
- 快速校验通知来源和签名。
- 将原始通知持久化到数据库或消息队列。
- 立即返回第三方要求的确认响应。
- 在后台更新订单并处理其他业务。
- 使用第三方交易号实现幂等,允许第三方重复发送同一通知。
如果业务处理超过了第三方设置的 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 Permanently302 Found307 Temporary Redirect308 Permanent Redirect401 Unauthorized403 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的赋值方式。
备注:内容仅供参考。
