You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js+React+MongoDB环境下Stripe嵌入式结账404错误求助

问题

在Node.js、React和MongoDB环境中搭建Stripe嵌入式结账功能时,点击结账按钮后出现以下错误:

  • POST http://localhost:5173/api/embedded-checkout 404 (Not Found)
  • IntegrationError: fetchClientSecret失败,收到非JSON响应(返回HTML内容)
  • 等待client secret超时

相关代码

嵌入式结账路由代码

import express from 'express';
const router = express.Router();
import Stripe from 'stripe';

router.post('/api/embedded-checkout', async (req, res) => {
  try {
    const { priceId } = req.body;

    const session = await Stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
        },
      ],
      mode: 'subscription',
      success_url: `${req.headers.origin}/return?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/cancel`,
    });

    res.json({ id: session.id, client_secret: session.client_secret });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: error.message });
  }
});
export default router;

React组件代码

import { loadStripe } from "@stripe/stripe-js";
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from "@stripe/react-stripe-js";
import { useCallback, useRef, useState } from "react";

export default function EmbeddedCheckoutButton() {
  const stripePromise = loadStripe("pk_live_51P6gXHAOg135X54QQlQBrJkIn5nA79aKUj4s0iHnW9UYMfyo7VnJCT8OYNXSWP9UIHwotu21DsKBxbHEHMfwGhMP00DSgMa86o");

  const [showCheckout, setShowCheckout] = useState(false);
  const modalRef = useRef(null);

  const fetchClientSecret = useCallback(() => {
    return fetch("/api/embedded-checkout", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ priceId: "price_1P7D6PAOg135X54QFHUDmSgG" }),
    })
    .then((res) => res.json())
    .then((data) => data.client_secret);
  }, []);

  const options = { fetchClientSecret };

  const handleCheckoutClick = () => {
    setShowCheckout(true);
    modalRef.current && modalRef.current.showModal();
  };

  const handleCloseModal = () => {
    setShowCheckout(false);
    modalRef.current && modalRef.current.close();
  };

  return (
    <>
      <button onClick={handleCheckoutClick}>Open Modal with Embedded Checkout</button>
      <dialog ref={modalRef} onClose={handleCloseModal}>
        <h3>Embedded Checkout</h3>
        {showCheckout && (
          <EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
            <EmbeddedCheckout />
          </EmbeddedCheckoutProvider>
        )}
        <button onClick={handleCloseModal}>Close</button>
      </dialog>
    </>
  );
}

主服务文件代码

import express from "express";
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import userRoutes from './routes/user.route.js';
import authRoutes from './routes/auth.route.js';
import stripeRoutes from './routes/stripe.route.js';
import stripeEmbeddedRoutes from './routes/embedded-checkout.route.js';

dotenv.config();

mongoose.connect(process.env.MONGO)
.then(() => {
  console.log('Connected to MongoDB');
})
.catch((err) => {
  console.log(err);
});

const app = express();

app.use(express.json());

app.use('/api/user', userRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/stripe', stripeRoutes);
app.use('/api/stripe/success', stripeRoutes);
app.use('/api/stripe/cancel', stripeRoutes);
app.use ('/api/embedded-checkout', stripeEmbeddedRoutes);
app.listen(3000, () => {
  console.log('server is running on port 3000!');
});

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';
  console.log(err);
  return res.status(statusCode).json({
    success: false,
    message,
    statusCode,
  })
})
解决方案

1. 修复404路由不匹配问题

问题根源:

  • 前端React运行在localhost:5173,后端在localhost:3000,前端直接请求/api/embedded-checkout会指向前端服务器,导致404;
  • 后端路由重复挂载路径:路由文件中定义了/api/embedded-checkout,主服务又挂载了/api/embedded-checkout前缀,实际有效路径变成/api/embedded-checkout/api/embedded-checkout,完全不匹配。

修复步骤:

前端处理(二选一):

  • 方式一:直接指定后端地址
    修改React组件中的fetch URL:
    return fetch("http://localhost:3000/api/embedded-checkout", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ priceId: "price_1P7D6PAOg135X54QFHUDmSgG" }),
    })
    
  • 方式二:配置Vite代理(推荐,避免跨域)
    在vite.config.js中添加代理配置:
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    
    export default defineConfig({
      plugins: [react()],
      server: {
        proxy: {
          '/api': {
            target: 'http://localhost:3000',
            changeOrigin: true
          }
        }
      }
    })
    

后端路由修改:

将路由文件中的路径改为根路径,因为主服务已经挂载了前缀:

// 原代码
router.post('/api/embedded-checkout', async (req, res) => { ... })
// 修改为
router.post('/', async (req, res) => { ... })

2. 修复Stripe实例初始化缺失秘钥问题

问题根源:

后端代码中未传入Stripe秘钥(sk_开头)就调用API,导致请求失败,返回HTML错误页面而非JSON,引发fetchClientSecret失败;同时line_items中缺失必填的quantity参数,也会导致API调用失败。

修复步骤:

  1. 在.env文件中添加Stripe秘钥:
    STRIPE_SECRET_KEY=sk_your_secret_key_here
    
  2. 在嵌入式结账路由文件中初始化Stripe实例并补充必填参数:
    import express from 'express';
    const router = express.Router();
    import Stripe from 'stripe';
    
    // 初始化Stripe实例
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
    
    router.post('/', async (req, res) => {
      try {
        const { priceId } = req.body;
        
        // 校验必填参数
        if (!priceId) {
          return res.status(400).json({ message: 'priceId参数缺失' });
        }
    
        // 使用初始化后的stripe实例调用API,补充quantity参数
        const session = await stripe.checkout.sessions.create({
          payment_method_types: ['card'],
          line_items: [
            {
              price: priceId,
              quantity: 1,
            },
          ],
          mode: 'subscription',
          success_url: `${req.headers.origin}/return?session_id={CHECKOUT_SESSION_ID}`,
          cancel_url: `${req.headers.origin}/cancel`,
        });
    
        res.json({ id: session.id, client_secret: session.client_secret });
      } catch (error) {
        console.error(error);
        res.status(500).json({ message: error.message });
      }
    });
    export default router;
    

3. 等待Client Secret超时问题

此问题是前两个错误的连锁反应,修复上述两个问题后,前端能正常获取client_secret,超时问题会自动解决。

额外优化建议

  • 前端Stripe公钥建议从环境变量读取,避免硬编码:
    // .env文件
    VITE_STRIPE_PUBLIC_KEY=pk_your_public_key_here
    // React组件中
    const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLIC_KEY);
    

内容的提问来源于stack exchange,提问作者user23114523

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 09:02:13