PAYATHON 2026

Heroku Backend 无法接收 fetch POST 请求

支付老李

结论

/payment 路由收到请求后,没有向客户端返回任何响应。代码执行到:

const { products, email } = req.body;

之后没有调用 res.json()、res.send() 或 res.end(),也没有进入错误处理中间件,所以连接一直处于打开状态。Heroku Router 等待约 30 秒后仍未收到响应,便终止请求、记录 H12 Request timeout,并返回 503。

如果实际路由还调用了 Stripe、数据库或其他外部 API,这些操作长时间没有完成,也会触发 H12。不过就目前给出的代码来看,最直接的问题是路由没有返回响应。

正确处理请求

先在路由之前启用 JSON 请求体解析,并确保请求无论成功还是失败都会结束:

const express = require('express');
const cors = require('cors');

const app = express();

app.use(cors());
app.use(express.json());

if (process.env.NODE_ENV === 'production') {
  app.use(express.static('clients/build'));
}

app.post('/payment', async (req, res) => {
  try {
    const { products, email } = req.body;

    if (!Array.isArray(products) || !email) {
      return res.status(400).json({
        error: 'products and email are required'
      });
    }

    // 在这里执行支付会话创建、数据库写入等操作
    // const session = await createCheckoutSession(products, email);

    return res.status(200).json({
      success: true
      // sessionId: session.id
    });
  } catch (error) {
    console.error('POST /payment failed:', error);

    return res.status(500).json({
      error: 'Unable to create payment session'
    });
  }
});

app.listen(process.env.PORT || 4000, () => {
  console.log('Server Start');
});

express.json() 必须放在 /payment 路由之前。否则,客户端发送 Content-Type: application/json 时,req.body 可能是 undefined。

此时直接解构 req.body:

const { products, email } = req.body;

还可能抛出异常。如果项目没有统一的 Express 错误处理,客户端收到的结果会更加混乱。

前端也要处理服务端响应

fetch() 只会在网络请求本身失败时自动抛出异常。即使服务端返回 400、500 或 503,仍然需要主动检查 response.ok:

const response = await fetch(
  'https://e-commerce-shopping-1.herokuapp.com/payment',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json'
    },
    body: JSON.stringify({
      products,
      email: email !== null ? email : displayName
    })
  }
);

const data = await response.json();

if (!response.ok) {
  throw new Error(data.error || `Request failed: ${response.status}`);
}

console.log(data);

如果路由还调用了支付接口

支付路由通常还会调用 Stripe 等外部服务,例如:

app.post('/payment', async (req, res) => {
  try {
    const { products, email } = req.body;

    const session = await stripe.checkout.sessions.create({
      // 根据实际业务填写参数
    });

    return res.status(200).json({
      sessionId: session.id
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      error: error.message
    });
  }
});

排查时应注意以下几点:

  • await 等待的外部请求能否正常完成;
  • Heroku 中是否已经配置 Stripe Key 等环境变量;
  • 是否有 Promise 一直没有 resolve 或 reject;
  • 每条条件分支是否都调用了 res.json()、res.send()、res.end() 或 next(error);
  • 发送响应后是否遗漏 return,导致代码继续运行并尝试重复响应。

延长客户端等待时间不能解决 H12。Heroku 日志已经表明请求成功连接到了 dyno:

connect=0ms service=30005ms status=503

问题不在于服务器无法连接,而在于应用没有在约 30 秒内完成响应。

CORS 与静态文件不是主要原因

日志中已经出现:

method=POST path="/payment"

这说明 POST 请求确实到达了 Heroku Router。因此,当前 H12 的主要原因不是静态文件配置,也不太可能只是浏览器的 CORS 拦截。

如果前端和后端部署在不同域名,仍然应在后端统一配置 CORS:

app.use(cors({
  origin: 'https://your-frontend-domain.example',
  methods: ['GET', 'POST', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Accept']
}));

如果 React 页面和 API 由同一个 Heroku 应用提供,可以直接使用相对地址:

fetch('/payment', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json'
  },
  body: JSON.stringify({ products, email })
});

先补上 express.json(),再确认 /payment 的成功、参数错误和异常路径都会明确返回响应。如果修改后仍然出现 H12,再检查路由中尚未展示的支付接口、数据库查询或其他异步操作。

备注:内容仅供参考。