电脑网站调用page.pay后form表单提交出现404错误
结论
page.pay 能正常返回,只说明支付请求已生成,不代表支付已经成功。提交返回的 form 后出现 404,首先要确认是哪一个地址返回了 404:
- 尚未进入支付宝收银台就出现 404,应检查
form的action、支付宝网关地址,以及前端是否改写了返回内容。 - 完成支付后才出现 404,应检查
return_url是否配置错误、对应路由是否存在,以及该地址能否从公网访问。 notify_url配置错误通常不会让浏览器显示 404,但会导致支付结果的异步通知失败。
建议让服务端直接输出 SDK 返回的完整表单,不要解析、拼接或修改表单内容。同时确认正式环境和沙箱环境使用的网关、应用及密钥配置相互匹配。
先确认 404 出现在哪个阶段
打开浏览器开发者工具,在 “网络(Network)” 中找到真正返回 404 的请求地址。
提交表单后立即出现 404
先看请求是否发往支付宝网关。常见的正式环境网关形式为:
https://openapi.alipay.com/gateway.do
沙箱环境需要使用对应的沙箱网关。具体地址应以当前支付宝开放平台文档和 SDK 配置为准,不要自行拼接。
如果返回 404 的地址仍是自己的网站,例如:
https://www.example.com/gateway.do
通常说明 form action 使用了相对地址、网关配置不完整,或者表单在传递过程中被修改。
支付完成后出现 404
如果已经进入支付宝收银台,完成支付后才跳转到 404 页面,应检查 return_url。例如:
https://www.example.com/pay/alipay/return
逐项确认:
- 路由确实存在;
- 域名、端口和协议正确;
- 地址可以从公网访问;
- 反向代理已将该路径转发到应用;
- 应用没有把 GET 回跳请求错误地限制为 POST;
- 路径大小写及末尾斜杠与实际路由一致。
检查服务端网关配置
以 Java SDK 的常见调用方式为例,传给客户端的网关地址应是完整的绝对 URL:
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
appId,
merchantPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
还要核对以下配置:
appId是否属于当前环境;- 正式应用是否使用正式网关;
- 沙箱应用是否使用沙箱网关;
- 应用私钥和支付宝公钥是否配套;
- 签名算法是否与平台配置一致;
- 网关地址中是否有空格、换行、重复路径或错误端口。
不要把网关地址写成开放平台后台地址、应用域名或文档页面地址。
原样输出 SDK 返回的表单
AlipayTradePagePayResponse#getBody() 通常返回一段可以自动提交的 HTML 表单。服务端应将其作为 HTML 原样写入响应:
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://www.example.com/pay/alipay/return");
request.setNotifyUrl("https://www.example.com/pay/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "ORDER_202609130001",
"total_amount": "99.00",
"subject": "订单支付",
"product_code": "FAST_INSTANT_TRADE_PAY"
}
""");
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"page.pay 调用失败:" + response.getCode() + " / " + response.getSubMsg()
);
}
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(response.getBody());
不要对 response.getBody() 做这些处理:
- 提取表单字段后自行重组;
- 对整段 HTML 再做 URL 编码;
- 通过模板引擎进行 HTML 转义;
- 删除隐藏字段;
- 修改
action; - 把表单当作普通文本或 JSON 返回,再直接插入页面。
如果模板渲染后得到的是下面这种内容:
<form name="punchout_form" method="post" action="...">
说明 HTML 已被转义,浏览器不会把它识别为真正的表单。
前后端分离时的处理方式
如果后端通过 JSON 返回表单字符串,前端要确认该字符串没有被二次编码。可以创建一个临时页面,再写入完整 HTML:
const html = response.data.form;
const paymentWindow = window.open("", "_blank");
if (!paymentWindow) {
throw new Error("支付窗口被浏览器拦截");
}
paymentWindow.document.open();
paymentWindow.document.write(html);
paymentWindow.document.close();
更直接的做法是让浏览器访问后端的支付发起地址,由后端响应完整表单:
window.location.href = "/api/pay/alipay/page";
对应接口直接返回 text/html,可以减少转义、截断和框架过滤带来的问题。
如果必须使用 innerHTML,还要注意某些浏览器或前端框架不会执行通过 innerHTML 插入的 <script>。这会让 SDK 表单中的自动提交脚本失效,此时需要手动找到表单并提交:
const container = document.createElement("div");
container.innerHTML = response.data.form;
document.body.appendChild(container);
const form = container.querySelector("form");
if (!form) {
throw new Error("支付宝返回内容中未找到 form");
}
form.submit();
检查 return_url 和 notify_url
这两个地址的用途不同:
request.setReturnUrl("https://www.example.com/pay/alipay/return");
request.setNotifyUrl("https://www.example.com/pay/alipay/notify");
return_url是支付结束后的浏览器同步跳转地址,通常需要支持 GET 请求。notify_url是支付宝服务器发送异步通知的地址,通常需要支持 POST 请求。
示例路由:
@GetMapping("/pay/alipay/return")
public String alipayReturn(HttpServletRequest request) {
// 验签后查询订单状态,再展示支付结果
return "pay/result";
}
@PostMapping("/pay/alipay/notify")
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
// 验签并更新订单;处理成功后按接口要求返回成功响应
return "success";
}
不要只根据 return_url 中的参数把订单标记为已支付。同步跳转可能被用户关闭,也可能被伪造。最终支付状态应结合签名验证、异步通知和主动查询来确认。
检查反向代理和应用路由
如果 404 来自商户网站,还要检查 Nginx、网关和应用上下文路径。假设应用的实际接口是:
/pay/alipay/return
如果 Nginx 转发时删除或重复添加了 /api,最终请求可能变成:
/api/api/pay/alipay/return
可重点检查类似配置:
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
proxy_pass 是否带末尾 / 会影响路径的拼接方式,需要结合实际请求地址和后端路由判断。
推荐排查顺序
- 在 Network 中找到真正返回 404 的 URL。
- 确认错误发生在进入收银台之前,还是支付完成回跳之后。
- 查看返回表单中的
method和action,确认action是正确的支付宝绝对地址。 - 确认正式环境、沙箱环境和应用配置没有混用。
- 让服务端原样输出
pageExecute()返回的body。 - 排除模板转义、JSON 二次编码以及前端脚本未执行的问题。
- 验证
return_url对应的路由是否存在并支持 GET。 - 验证
notify_url是否能从公网访问并支持 POST。 - 检查 Nginx、网关和应用路由对路径的转换。
- 结合 SDK 返回的
code、subCode、msg、subMsg和服务端日志继续定位。
排查时可以记录订单号、请求时间、网关地址及 SDK 返回的错误信息,但不要在日志或截图中暴露应用私钥、签名原文和完整敏感参数。
备注:内容仅供参考。