PAYATHON 2026

支付宝沙箱支付回调通知中 request.Form 为空

支付老李

结论

支付宝异步通知通常通过 POST 提交,请求体格式为 application/x-www-form-urlencoded,不是 JSON。Request.Form 为空,多半与接收端解析、请求体被提前读取、通知地址发生跳转,或网关转发有关。这和沙箱支付是否成功是两个不同的问题。

可以向支付宝开放平台的官方支持渠道反馈,但在此之前,建议先保留请求证据并检查本地服务。只有确认支付宝发出的请求体确实为空,或者请求根本没有到达通知地址,平台侧才更容易定位问题。

常见原因

1. 实际收到的是同步跳转请求

return_url 用于浏览器同步跳转,notify_url 用于服务器异步通知,不能混用。

先确认日志中的请求确实发往 notify_url,再检查:

  • HTTP 方法是否为 POST
  • Content-Type 是否包含 application/x-www-form-urlencoded
  • 中间是否发生过 301、302、307 或 308 跳转
  • 请求是否被反向代理、WAF、网关或负载均衡器改写

尤其要留意 301 和 302 跳转。部分客户端会在跳转后把 POST 改为 GET,目标接口也就无法读取原来的表单内容。

2. 请求体已经被其他代码读取

在 ASP.NET Core 中,请求体通常只能按顺序读取一次。如果日志中间件、验签组件或业务代码已经读取过 Request.Body,后续访问 Request.Form 时可能拿不到预期内容。

如果需要读取原始请求体,应先调用 EnableBuffering(),读取完成后再把流的位置复原。

using System.Text;

app.Use(async (context, next) =>
{
    context.Request.EnableBuffering();

    using var reader = new StreamReader(
        context.Request.Body,
        Encoding.UTF8,
        detectEncodingFromByteOrderMarks: false,
        leaveOpen: true);

    string body = await reader.ReadToEndAsync();
    context.Request.Body.Position = 0;

    // 仅用于排查,生产环境不要直接记录完整支付参数。
    Console.WriteLine(body);

    await next();
});

这个中间件必须放在其他读取请求体的组件之前。

3. 按 JSON 方式绑定了支付宝通知

下面的写法通常不适合接收支付宝的表单通知:

[HttpPost]
public IActionResult Notify([FromBody] AlipayNotifyModel model)
{
    // [FromBody] 通常按 JSON 等请求体格式处理
}

应当直接读取表单字段:

[HttpPost]
public async Task<IActionResult> Notify()
{
    if (!Request.HasFormContentType)
    {
        return BadRequest("Unsupported Content-Type");
    }

    IFormCollection form = await Request.ReadFormAsync();

    var parameters = form.ToDictionary(
        item => item.Key,
        item => item.Value.ToString());

    if (parameters.Count == 0)
    {
        return BadRequest("Empty form");
    }

    // 使用支付宝 SDK 或符合当前接口规范的方式验签。
    bool verified = VerifyAlipaySignature(parameters);

    if (!verified)
    {
        return Content("failure", "text/plain");
    }

    // 验证 app_id、商户订单号、金额和交易状态,并保证业务处理幂等。
    ProcessPayment(parameters);

    return Content("success", "text/plain");
}

VerifyAlipaySignature 和 ProcessPayment 只是占位方法,需要替换为项目实际使用的支付宝 SDK 验签及订单处理逻辑。

4. 接口框架没有启用表单解析

如果使用传统 ASP.NET,可以直接检查:

public ActionResult Notify()
{
    var parameters = Request.Form.AllKeys
        .Where(key => key != null)
        .ToDictionary(
            key => key!,
            key => Request.Form[key] ?? string.Empty);

    if (parameters.Count == 0)
    {
        return Content("failure");
    }

    // 验签及业务处理
    return Content("success");
}

使用其他语言或框架时,也要调用对应的 URL 编码表单解析功能。例如,Java Servlet 应使用 request.getParameterMap(),不能按 JSON 反序列化处理。

5. 代理或安全组件丢弃了请求体

如果应用部署在 IIS、Nginx、API Gateway、Serverless 平台或内网穿透服务之后,应分别记录入口层和应用层的以下信息:

请求时间
请求 URL
HTTP 方法
Content-Type
Content-Length
User-Agent
响应状态码
是否发生重定向
应用实际读取到的请求体长度

如果代理日志中的 Content-Length 大于零,但应用读取结果为空,问题通常出在代理转发、服务器配置或应用中间件。只有入口层收到的请求体本身就是空的,才需要进一步核对支付宝侧的请求。

推荐排查步骤

  1. 确认支付宝接口配置的是公网可访问的 notify_url。该地址应直接返回处理结果,不应经过登录认证或页面跳转。
  2. 查看服务端访问日志,确认收到的是 POST,同时检查 Content-Type 和 Content-Length。
  3. 暂时停用可能读取请求体的日志、鉴权、WAF 或自定义中间件。
  4. 在控制器入口调用 ReadFormAsync(),检查读取到的表单字段数量。
  5. 使用 curl 模拟相同格式的请求,确认接收端能否正常解析:
curl -i -X POST 'https://example.com/alipay/notify' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
  --data-urlencode 'trade_no=TEST_TRADE_NO' \
  --data-urlencode 'out_trade_no=TEST_ORDER_NO' \
  --data-urlencode 'trade_status=TRADE_SUCCESS'

如果模拟请求也无法读取表单,基本可以确定问题出在本地应用或基础设施。如果模拟请求正常,则需要对比真实通知的访问日志和请求头。

是否需要联系支付宝支持

完成上述检查后,如果能够证明存在以下任一情况,可以通过支付宝开放平台的官方支持入口提交问题:

  • 支付宝侧显示已发送异步通知,但服务器入口日志中完全没有对应请求
  • 服务器明确收到了支付宝通知,但请求的 Content-Length 为零
  • 相同接口可以正确解析模拟表单,但真实通知始终不含任何业务字段
  • 沙箱控制台显示的通知状态、重试记录与服务器日志明显不一致

提交反馈时,建议附上沙箱应用标识、接口名称、通知时间、out_trade_no、支付宝交易号、通知地址、HTTP 状态码,以及脱敏后的访问日志。不要提交应用私钥、支付宝公钥的私密配置、完整签名材料或用户敏感信息。

注意事项

收到参数后,不能只根据 trade_status 更新订单。还必须完成签名验证,并核对 app_id、商户订单号、订单金额等重要字段。异步通知可能重复发送,因此订单处理必须具备幂等性。

处理成功后,应返回以下纯文本:

success

如果验签失败、参数异常或业务处理失败,不要返回 success。返回 HTML、JSON、额外空格或调试信息,都可能导致支付宝无法识别处理结果并继续重试。

备注:内容仅供参考。