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

Stripe /create-payment-intent接口POST 404报错求助

解决Stripe支付接口POST 404错误问题

问题描述

开发简易Stripe定期支付处理器时,调用/create-payment-intent接口持续出现POST 404错误,控制台报错如下。通过Insomnia测试该接口可正常响应,但前端集成时调用失败。

报错日志

POST http://localhost:5173/create-payment-intent 404 (Not Found)
(anonymous) @ App.jsx:18
commitHookEffectListMount @ react-dom_client.js?v=1b04ad3d:16904
invokePassiveEffectMountInDEV @ react-dom_client.js?v=1b04ad3d:18320
invokeEffectsInDev @ react-dom_client.js?v=1b04ad3d:19697
commitDoubleInvokeEffectsInDEV @ react-dom_client.js?v=1b04ad3d:19682
flushPassiveEffectsImpl @ react-dom_client.js?v=1b04ad3d:19499
flushPassiveEffects @ react-dom_client.js?v=1b04ad3d:19443
(anonymous) @ react-dom_client.js?v=1b04ad3d:19324
workLoop @ react-dom_client.js?v=1b04ad3d:197
flushWork @ react-dom_client.js?v=1b04ad3d:176
performWorkUntilDeadline @ react-dom_client.js?v=1b04ad3d:384
Show 10 more frames
Show less
VM149:1 Uncaught (in promise) SyntaxError: Unexpected end of JSON input
    at App.jsx:23:26

相关代码

App.jsx代码

import React, { useState, useEffect } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "./CheckoutForm";
import "./App.css";

// Make sure to call loadStripe outside of a component’s render to avoid
// recreating the Stripe object on every render.
// This is your test publishable API key.
const stripePromise = loadStripe("pk_test_51P0HqXLUshuizymb87sieUj8dR6IrsTqqQGINvIrdWpLDPgTMOEM1OkvnDpPGyxa4lUv7tXDlShU5SJjdV3RduZg00Y6RRHSi8");

export default function App() {
  const [clientSecret, setClientSecret] = useState("");

  useEffect(() => {
    // Create PaymentIntent as soon as the page loads
    fetch("/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, []);

  const appearance = {
    theme: 'stripe',
  };
  const options = {
    clientSecret,
    appearance,
  };

  return (
    <div className="App">
      {clientSecret && (
        <Elements options={options} stripe={stripePromise}>
          <CheckoutForm />
        </Elements>
      )}
    </div>
  );
}

CheckoutForm.jsx代码

import React, { useEffect, useState } from "react";
import {
  PaymentElement,
  useStripe,
  useElements
} from "@stripe/react-stripe-js";

export default function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const [message, setMessage] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!stripe) {
      return;
    }

    const clientSecret = new URLSearchParams(window.location.search).get(
      "payment_intent_client_secret"
    );

    if (!clientSecret) {
      return;
    }

    stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
      switch (paymentIntent.status) {
        case "succeeded":
          setMessage("Payment succeeded!");
          break;
        case "processing":
          setMessage("Your payment is processing.");
          break;
        case "requires_payment_method":
          setMessage("Your payment was not successful, please try again.");
          break;
        default:
          setMessage("Something went wrong.");
          break;
      }
    });
  }, [stripe]);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js hasn't yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setIsLoading(true);

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        // Make sure to change this to your payment completion page
        return_url: "http://localhost:3000",
      },
    });

    // This point will only be reached if there is an immediate error when
    // confirming the payment. Otherwise, your customer will be redirected to
    // your `return_url`. For some payment methods like iDEAL, your customer will
    // be redirected to an intermediate site first to authorize the payment, then
    // redirected to the `return_url`.
    if (error.type === "card_error" || error.type === "validation_error") {
      setMessage(error.message);
    } else {
      setMessage("An unexpected error occurred.");
    }

    setIsLoading(false);
  };

  const paymentElementOptions = {
    layout: "tabs"
  }

  return (
    <form id="payment-form" onSubmit={handleSubmit}>

      <PaymentElement id="payment-element" options={paymentElementOptions} />
      <button disabled={isLoading || !stripe || !elements} id="submit">
        <span id="button-text">
          {isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}
        </span>
      </button>
      {/* Show any error or success messages */}
      {message && <div id="payment-message">{message}</div>}
    </form>
  );
}

server.js代码

const express = require("express");
const app = express();
require("dotenv").config();
const stripe = require("stripe")(process.env.SECRET_KEY);

app.use(express.static("public"));
app.use(express.json());

const calculateOrderAmount = (items) => {
  // Your calculation logic here
  return 1400; // Example amount, replace with actual calculation
};

app.post("/create-payment-intent", async (req, res) => {
  const { items } = req.body;

  try {
    // Create a PaymentIntent with the order amount and currency
    const paymentIntent = await stripe.paymentIntents.create({
      amount: calculateOrderAmount(items),
      currency: "usd",
      // In the latest version of the API, specifying the `automatic_payment_methods` parameter is optional because Stripe enables its functionality by default.
      automatic_payment_methods: {
        enabled: true,
      },
    });

    res.json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    console.error("Error creating payment intent:", error);
    res.status(500).json({ error: "Failed to create payment intent" });
  }
});

app.listen(5000, () => {
  console.log("Server is running on http://localhost:5000");
});

问题原因与解决方案

核心原因

前端开发服务器运行在localhost:5173,后端服务器运行在localhost:5000,前端代码中使用相对路径/create-payment-intent发起请求时,会直接请求前端服务器的5173端口,而非后端的5000端口,导致404错误。而Insomnia测试正常是因为直接请求了后端的5000端口。

解决方案

有两种可行方案,任选其一即可:

方案1:配置前端代理(推荐)

以Vite为例,在项目根目录的vite.config.js中添加代理配置,将前端的API请求转发到后端服务器:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/create-payment-intent': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  }
})

配置完成后重启前端开发服务器,此时前端的相对路径请求会自动转发到后端5000端口。

方案2:修改请求为绝对路径

直接修改App.jsx中的fetch URL为后端服务器的完整地址:

fetch("http://localhost:5000/create-payment-intent", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
})

额外优化点

检查CheckoutForm.jsx中的return_url,当前设置为http://localhost:3000,与前端实际运行的5173端口不符,建议修改为http://localhost:5173,避免支付完成后跳转错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 18:08:11