支付宝异步通知报错,但 HTTP 状态码为 200,如何排查?
结论
HTTP 200 OK 只能说明请求到达了 Web 服务,服务器也完成了 HTTP 层的响应。它不代表支付宝已经确认通知处理成功。
从给出的 curl -v 结果来看,响应头后面没有正文,接口很可能返回了空内容。支付宝异步通知通常要求商户完成验签和业务处理后,在响应正文中明确返回以下纯文本:
success
如果接口返回空字符串、HTML 页面、JSON、报错信息、重定向结果,或者在 success 前后夹带调试内容,即使 HTTP 状态码是 200,支付宝仍可能认为通知失败并继续重试。
为什么 200 OK 仍然会报错
异步通知成功通常要同时满足两个条件:
- HTTP 请求能够正常到达,接口返回可接受的 HTTP 状态码。
- HTTP 响应正文符合支付宝要求,通常应为纯文本
success。
当前测试只能证明以下几点:
- 域名可以解析;
18880端口可以连接;- nginx 和 PHP 可以处理请求;
- 接口返回了 HTTP
200。
但输出中没有正文:
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
<
这说明 notify.php 处理 test=ok 时很可能没有输出任何内容。状态码为 200,不会让空响应自动成为成功确认。
另外,这次 curl 是从当前服务器主动访问接口,只验证了这一条网络路径,不能完全排除支付宝通知服务器访问时遇到以下情况:
- 来源 IP 被防火墙、WAF 或安全组拦截;
- 请求受到限流或命中反爬规则;
- IPv4、IPv6 或不同 DNS 节点的解析结果不一致;
- 接口处理时间过长,支付宝侧已经超时;
- HTTP 请求被重定向到 HTTPS、登录页或错误页;
- 真实通知参数触发了 PHP 异常,而
test=ok没有进入相同的代码分支。
先检查真实响应正文
不要只看状态码,还要检查响应头和响应体:
curl -sS \
-D /tmp/alipay_headers.txt \
-o /tmp/alipay_body.txt \
-w '\nHTTP_CODE=%{http_code}\nTIME_TOTAL=%{time_total}\n' \
'http://alipay.example.com:18880/alipay/notify.php' \
--data 'test=ok'
cat /tmp/alipay_headers.txt
xxd -g 1 /tmp/alipay_body.txt
理想情况下,响应体的字节内容应接近:
00000000: 73 75 63 63 65 73 73 success
末尾换行通常未必会导致失败,但最稳妥的方式是只输出 success,不要附加 HTML、JSON、日志、空格或调试文本。
还要检查接口是否发生了重定向:
curl -sS -o /dev/null -D - \
'http://alipay.example.com:18880/alipay/notify.php' \
--data 'test=ok'
如果返回 301、302、307 或 308,应把支付宝通知地址直接配置为最终处理地址,不要依赖重定向。
正确的通知处理流程
接口收到通知后,应按以下顺序处理:
- 读取支付宝实际提交的表单参数。
- 使用支付宝 SDK 和支付宝公钥进行 RSA2 验签。
- 校验
app_id、商户身份、订单号、金额和交易状态。 - 根据
out_trade_no进行幂等处理。 - 持久化本地业务处理结果。
- 所有步骤成功后,返回纯文本
success。 - 如果验签或业务处理失败,不要返回
success,以便继续排查或等待重试。
不能只看到 trade_status=TRADE_SUCCESS 就更新订单。签名、订单归属和金额都必须验证。
PHP 处理示例
下面的代码只展示处理结构。SDK 加载路径、公钥配置和业务字段需要按实际项目调整,不能直接照搬占位值:
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
require_once __DIR__ . '/vendor/autoload.php';
$params = $_POST;
if (empty($params)) {
http_response_code(400);
echo 'failure';
exit;
}
try {
$aop = new AopClient();
$aop->alipayrsaPublicKey = '支付宝公钥内容';
$aop->signType = 'RSA2';
// 使用 SDK 验签,避免自行拼接待验签字符串。
$verified = $aop->rsaCheckV1($params, null, 'RSA2');
if (!$verified) {
error_log('Alipay notify signature verification failed');
echo 'failure';
exit;
}
$expectedAppId = '实际应用 app_id';
if (($params['app_id'] ?? '') !== $expectedAppId) {
error_log('Alipay notify app_id mismatch');
echo 'failure';
exit;
}
$outTradeNo = $params['out_trade_no'] ?? '';
$tradeNo = $params['trade_no'] ?? '';
$tradeStatus = $params['trade_status'] ?? '';
$totalAmount = $params['total_amount'] ?? '';
if ($outTradeNo === '' || $tradeNo === '') {
error_log('Alipay notify missing order identifier');
echo 'failure';
exit;
}
// 从数据库查询订单,并校验订单归属和金额。
// 金额应使用十进制定点数或以“分”为单位比较,不要使用 float 直接比较。
$order = findOrderByOutTradeNo($outTradeNo);
if ($order === null) {
error_log('Alipay notify order not found: ' . $outTradeNo);
echo 'failure';
exit;
}
if (!amountEquals($order['total_amount'], $totalAmount)) {
error_log('Alipay notify amount mismatch: ' . $outTradeNo);
echo 'failure';
exit;
}
if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
// 必须幂等:同一通知重复到达时不能重复发货、重复记账。
markOrderPaidIdempotently($outTradeNo, $tradeNo, $params);
}
echo 'success';
} catch (Throwable $e) {
error_log('Alipay notify exception: ' . $e->getMessage());
// 未完成业务处理时不要返回 success。
http_response_code(500);
echo 'failure';
}
不同版本的 SDK 可能采用不同的方法签名,应以项目实际安装版本的接口定义为准。验签时要使用 SDK 和原始通知参数,不要修改参数后再验签。
检查参数接收方式
支付宝异步通知一般通过 application/x-www-form-urlencoded 提交,因此应优先读取:
$params = $_POST;
不要只读取 URL 查询参数:
$params = $_GET; // 通常不适合作为异步通知的唯一参数来源
题目中的「通知内容」以完整 URL 加查询字符串的形式展示,但这可能只是平台日志的展示方式,不能据此认定支付宝真实请求使用了 GET。应结合 nginx 访问日志、应用日志或抓包结果,确认实际的请求方法、请求头和请求体。
如果框架统一按 JSON 解析请求,也要留意支付宝通知通常不是 JSON 请求:
$json = json_decode(file_get_contents('php://input'), true);
如果只按 JSON 读取,结果可能是 null。程序随后仍可能返回 200,却没有完成验签和订单处理。
检查 PHP 是否报错但仍返回 200
以下问题都可能让接口返回 HTTP 200,同时产生空响应或异常页面:
display_errors已关闭,PHP 错误只写入日志;- 代码提前执行
return或exit,没有输出success; - 验签失败的分支没有任何输出;
- 数据库异常被捕获后,框架仍统一返回
200; - nginx 使用
error_page把后端错误转换成200; - 输出缓冲被清理;
- PHP 文件含有 BOM、调试输出或框架模板包装;
- 业务处理超时,支付宝侧断开后程序才执行完毕。
应检查通知时间附近的日志:
grep '11/Aug/2025:22:32' /var/log/nginx/access.log
grep '2025-08-11 22:32' /var/log/nginx/error.log
PHP-FPM 和应用日志的具体位置取决于实际部署配置。检查时需要确认:
- 请求是否确实到达;
- 请求方法和状态码是什么;
- 返回了多少响应字节;
- 总处理时间是多少;
- PHP 是否出现异常;
- 验签是否成功;
- 订单更新是否提交成功。
日志中不要保存完整签名、用户标识或未经脱敏的支付数据。
用真实请求结构复现
test=ok 只能测试最基本的连通性,无法触发真实的验签、金额校验和订单更新逻辑。应在测试环境中使用完整且未脱敏的原始通知参数复现问题。
参数中包含 &、$、中文或 JSON 字符串时,不要直接拼接未经转义的 Shell 命令。可以使用单引号和 --data-urlencode:
curl -i -sS \
'http://alipay.example.com:18880/alipay/notify.php' \
--data-urlencode 'out_trade_no=测试订单号' \
--data-urlencode 'trade_status=TRADE_SUCCESS' \
--data-urlencode 'total_amount=0.01' \
--data-urlencode 'sign_type=RSA2' \
--data-urlencode 'sign=完整签名'
手工修改任何已签名参数都会导致验签失败。因此,要真正验证验签流程,必须使用同一次通知的完整原始参数。题目中已经打码的内容不能用于验签,也不能重新排列参数或错误解码参数值。
推荐的定位顺序
按以下顺序排查通常更快:
- 在 nginx 访问日志中找到
2025-08-11 22:32:00附近的真实请求。 - 确认请求方法、状态码、响应字节数和处理耗时。
- 检查真实响应正文是否为纯文本
success。 - 排查重定向、WAF 拦截、超时和来源限制。
- 确认程序读取的是 POST 表单参数,而不是只读取 GET 或 JSON。
- 记录验签结果和失败原因。
- 校验
app_id、订单号、金额和交易状态。 - 检查数据库事务是否提交,业务逻辑是否抛出异常。
- 确认程序可以幂等处理重复通知,并再次返回
success。
根据现有信息,最明显的问题是接口虽然返回了 200 OK,但测试结果中没有出现 success 响应正文。应先修正并验证响应内容,再查看真实通知时刻的访问日志和应用日志,继续排查验签与业务处理流程。
备注:内容仅供参考。
