PAYATHON 2026

POST API call 与 form submission 有什么区别?

支付老李

明确结论

可以用 Node.js 的 HTTP 客户端向同一地址发送 POST 请求,但它未必能替代浏览器提交表单。

两者都可能发送 HTTP POST 请求,区别主要在于请求由谁发起,以及响应由谁处理:

  • 浏览器提交表单后,会加载响应页面或跟随重定向,所以用户能看到页面跳转。
  • Node.js 从服务器端发送请求,响应只会返回给 Node.js。用户的浏览器不知道这次请求,也就不会跳转。

接入支付网关前,需要先确认它提供的是浏览器收银台接口,还是服务端 API。如果网关要求用户进入其支付页面,通常仍需让浏览器提交表单,或访问网关返回的支付 URL。

form submission 实际做了什么

以下表单:

<form method="post" action="https://payment.example.com/checkout">
  <input type="hidden" name="order_id" value="ORDER_123">
  <input type="hidden" name="amount" value="100.00">
  <button type="submit">支付</button>
</form>

提交后,浏览器会:

  1. 根据 action 确定请求地址。
  2. 根据 method 发送 POST 请求。
  3. 按照表单的 enctype 编码字段。
  4. 接收支付网关返回的 HTML 或重定向响应。
  5. 在当前页面加载响应结果,或者跳转到新地址。

普通表单默认使用:

Content-Type: application/x-www-form-urlencoded

请求体大致如下:

order_id=ORDER_123&amount=100.00

这里的「跳转」是浏览器处理响应后产生的导航行为,不是 POST 请求本身具备跳转能力。

Node.js POST 请求有什么不同

Node.js 发出的 POST 请求发生在你的服务器与支付网关之间:

用户浏览器 -> 你的服务器 -> 支付网关

支付网关返回的 HTML、状态码或 Location 响应头,只会由 Node.js 程序接收。即使网关返回:

HTTP/1.1 302 Found
Location: https://payment.example.com/pay/session-123

用户的浏览器也不会自动进入该地址。你的程序需要读取响应,再决定如何通知浏览器跳转。

不同的 Node.js 客户端处理重定向的方式也不一样:

  • 原生 http、https 模块通常需要自行处理 3xx 响应。
  • fetch 等客户端可以跟随重定向,但执行跳转的仍是服务器端请求,不是用户浏览器。

使用 Node.js 发送相同格式的 POST 请求

如果支付网关允许从服务端调用,并要求使用 application/x-www-form-urlencoded,可以这样发送:

import https from 'node:https';
import { URLSearchParams } from 'node:url';

const body = new URLSearchParams({
  order_id: 'ORDER_123',
  amount: '100.00'
}).toString();

const request = https.request(
  {
    hostname: 'payment.example.com',
    path: '/checkout',
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Content-Length': Buffer.byteLength(body)
    }
  },
  (response) => {
    console.log('Status:', response.statusCode);
    console.log('Location:', response.headers.location);

    let responseBody = '';

    response.setEncoding('utf8');
    response.on('data', (chunk) => {
      responseBody += chunk;
    });

    response.on('end', () => {
      console.log(responseBody);
    });
  }
);

request.on('error', (error) => {
  console.error(error);
});

request.write(body);
request.end();

这段代码能发送与普通表单格式相近的请求,但响应仍停留在 Node.js 服务端。

接口被称为 API,并不意味着请求体应该改成 JSON。只有支付网关明确支持时,才能使用下面这种格式:

const body = JSON.stringify({
  order_id: 'ORDER_123',
  amount: '100.00'
});
Content-Type: application/json

application/json 和 application/x-www-form-urlencoded 是两种不同的请求格式。网关支持其中一种,不代表它也支持另一种。

如何让用户进入支付页面

具体做法取决于支付网关的接入协议。

方式一:由浏览器直接提交表单

如果支付网关文档给出了表单字段,并要求将用户提交到其收银台,可以继续使用表单。敏感签名应由服务端生成,然后将支付参数返回给页面。

<form id="payment-form" method="post" action="https://payment.example.com/checkout">
  <input type="hidden" name="order_id" value="ORDER_123">
  <input type="hidden" name="amount" value="100.00">
  <input type="hidden" name="signature" value="SERVER_GENERATED_SIGNATURE">
</form>

<script>
  document.getElementById('payment-form').submit();
</script>

这样,浏览器会通过 POST 请求进入支付网关页面。

在真实项目中,不要把未经转义的用户输入直接拼接到 HTML 中,否则可能引发 XSS 风险。

方式二:服务端创建支付会话,再让浏览器跳转

很多现代支付网关要求服务端先调用 API 创建支付会话:

浏览器 -> 你的服务器 -> 支付 API
                    <- 返回 checkout_url
浏览器 <- checkout_url
浏览器 -> 支付页面

Express 示例:

app.post('/create-payment', async (req, res) => {
  const gatewayResponse = await fetch(
    'https://api.payment.example.com/payment-sessions',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.PAYMENT_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        order_id: 'ORDER_123',
        amount: 10000,
        currency: 'CNY'
      })
    }
  );

  if (!gatewayResponse.ok) {
    return res.status(502).json({
      error: 'Failed to create payment session'
    });
  }

  const payment = await gatewayResponse.json();

  res.json({
    checkoutUrl: payment.checkout_url
  });
});

浏览器拿到地址后再跳转:

const response = await fetch('/create-payment', {
  method: 'POST'
});

const payment = await response.json();

window.location.assign(payment.checkoutUrl);

示例中的字段名、金额单位和返回结构只用于说明流程。实际参数必须以支付网关文档为准。

服务端也可以直接向浏览器返回重定向:

res.redirect(303, payment.checkout_url);

303 See Other 表示浏览器接下来会使用 GET 请求访问目标地址,适合 POST 创建资源后再跳转的场景。前提是目标支付页面支持通过 GET URL 打开。

方式三:返回自动提交的中转表单

如果网关必须接收浏览器发出的 POST,而服务端又需要提前准备签名参数,可以由服务端返回一个自动提交的 HTML 表单:

app.post('/start-payment', async (req, res) => {
  const orderId = 'ORDER_123';
  const amount = '100.00';
  const signature = createPaymentSignature({ orderId, amount });

  res.type('html').send(`
    <!doctype html>
    <html lang="zh-CN">
      <head>
        <meta charset="utf-8">
        <title>正在跳转到支付页面</title>
      </head>
      <body>
        <form id="payment-form"
              method="post"
              action="https://payment.example.com/checkout">
          <input type="hidden" name="order_id" value="${escapeHtml(orderId)}">
          <input type="hidden" name="amount" value="${escapeHtml(amount)}">
          <input type="hidden" name="signature" value="${escapeHtml(signature)}">
        </form>
        <script>
          document.getElementById('payment-form').submit();
        </script>
      </body>
    </html>
  `);
});

最终向支付网关发送 POST 的仍是用户浏览器,因此网关返回的页面可以正常显示给用户。

为什么不能简单地把网关响应转发给浏览器

Node.js 理论上可以获取支付网关返回的 HTML,再把它发送给用户,但这种做法通常不可靠。

支付页面可能依赖:

  • 网关域名下的 Cookie 和会话。
  • 使用相对路径加载的 CSS、JavaScript 和图片资源。
  • Content Security Policy。
  • 防跨站请求伪造参数。
  • 浏览器指纹或风控信息。
  • 网关设置的安全响应头。
  • 必须在支付网关自身域名下运行的后续页面。

除非支付网关明确支持这种集成方式,否则不要自行代理或改写托管支付页面。

接入支付网关时需要注意

严格按照网关文档选择接口

同一个支付平台可能同时提供:

  • 浏览器表单接口。
  • 服务端创建订单 API。
  • 托管收银台 URL。
  • 前端 JavaScript SDK。
  • App SDK。
  • 支付结果查询接口。
  • 异步 webhook 通知。

这些接口的用途不同,不能仅凭它们都使用 POST 就相互替换。

不要把密钥放在浏览器中

API key、签名私钥和 webhook secret 应保存在服务端。浏览器只能获取完成支付所需的公开参数或一次性支付会话信息。

不要只依赖支付完成后的页面跳转

用户支付成功后可能直接关闭页面,跳转请求也可能因网络问题失败。订单状态应以经过签名验证的 webhook 为准,也可以根据服务端主动查询到的支付结果确认。

正确处理金额和重复请求

金额最好按照支付网关的要求使用最小货币单位,例如分,以免产生浮点数误差。创建支付订单时还应使用唯一订单号或 idempotency key,避免网络重试造成重复扣款。

校验签名和回调参数

不要因为浏览器传来了 success=true 就更新订单状态。服务端必须验证支付结果的签名,并核对订单号、金额、币种和商户身份。

总结

POST 只是 HTTP 请求方法,并不负责页面跳转。表单提交后能显示新页面,是因为请求由浏览器发起,响应也由浏览器加载。Node.js 发起的 POST 属于服务器之间的通信,响应只会回到 Node.js。

如果支付网关要求用户进入托管收银台,通常可以选择以下方式:

  • 让浏览器直接提交网关要求的 POST 表单。
  • 由服务端调用支付 API 创建会话,再把支付 URL 返回给浏览器。
  • 由服务端生成参数和签名,再返回自动提交的中转表单。

具体实现应以支付网关的接入文档为准,尤其要确认请求格式、签名方式、浏览器跳转流程和异步通知机制。

备注:内容仅供参考。