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

Stripe+React+Express创建结账会话出现Cannot POST错误求解

问题背景

我在React应用中使用Stripe预制的结账流程实现商品下单支付功能,已开发基础购物车模块,现在希望实现「去结账」按钮功能:将购物车商品发送到Express服务端的POST路由,由服务端重定向到Stripe结账页面。

遇到的问题
  • 最初点击表单提交按钮发起POST请求时,返回Cannot POST /cart报错,无其他错误信息;但使用Postman可以正常访问该POST路由,其他GET API路由也运行正常。
  • 调整表单绑定方式,将method+action改为绑定onSubmit事件后,goToCheckout函数可被触发,但点击按钮后页面刷新,URL后缀新增?符号,未正常执行fetch请求跳转到Stripe结账页,添加event.preventDefault()也未生效。
  • 确认服务端/create-checkout-session路由本身运行正常,Postman请求可正常返回Stripe结账跳转链接。
相关代码

cart-page.js(负责购物车逻辑及表单提交代码,含goToCheckout定义)

import React from "react";

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

//* Allows Stripe to authentificate our API requests with our key
const stripePublishableKey = nodeEnv ? process.env.REACT_APP_stripe_dev_publishable_key : process.env.REACT_APP_stripe_prod_pubishable_key;
const stripe = require('stripe')(stripePublishableKey);

const CartPage = (props) => {

    const { cart, onAdd, onRemove } = props;

    const productTotal = cart.reduce((a, c) => a + c.unit_amount * c.qty, 0) // default value 0
    const taxTotal = <p>Tax is included in the price.</p>
    const shippingTotal = <p>You can choose your shipping options at checkout.</p>
    const totalCost = productTotal;

    const checkoutData = cart.map(item => (
        { 
            price: item.id, 
            quantity: item.qty,
        }
    ));

    const goToCheckout = async () => {

        // Call your backend to create the Checkout Session
        await fetch('/create-checkout-session', {
            method: "POST",
            headers: {
                    "Content-Type": "application/json"
                },
            body: JSON.stringify({
                items: [
                    checkoutData
                ]
            }),
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(session) {
            return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function(result) {
            // If `redirectToCheckout` fails due to a browser or network
            // error, you should display the localized error message to your
            // customer using `error.message`.
            if (result.error) {
                alert(result.error.message);
            }
        })
        .catch((error) => {
            console.error(error);
        });
    };

    const currencyFormatter = new Intl.NumberFormat('en-gb', {
        style:"currency", 
        currency:"GBP"
    }) 

    return (
        <main>

            <h1>Your Cart</h1>

            {cart.length === 0 && <p>Your Cart is Empty...</p>}

            {cart.map((item) => (
                <section className='cart-item' key={item.product.id}>
                    <h4>{item.product.name}</h4>
                        
                    <section className='cart-item-buttons'>
                        <button onClick={() => onAdd(item)}>+</button>
                        <button onClick={() => onRemove(item)}>-</button>
                    </section>
                        
                    <p>{item.qty} * {currencyFormatter.format(item.unit_amount / 100)}</p>
                </section>
            ))}

            {cart.length !== 0 && (
                <section>
                    <p>Total Product Price: {currencyFormatter.format(productTotal / 100)}</p>
                    <p>Toal Tax: {taxTotal}</p>
                    <p>Shipping Costs: {shippingTotal}</p>
                    <p><strong>Total Costs: {currencyFormatter.format(totalCost / 100)}</strong></p>
                </section>
            )}

            {cart.length > 0 && (
                <section>
                    <p>ADD CHECKOUT BUTTON</p>

                    <form method='POST' action={goToCheckout}>
                        <input type='submit' value='Go To Checkout' />
                    </form>

                </section>
            )}

        </main>
    );
};

export default CartPage;

createCheskoutSession.js(/create-checkout-session路由逻辑实现)

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

const YOUR_DOMAIN = nodeEnv ? process.env.REACT_APP_dev_domain : process.env.REACT_APP_prod_domain;

//* Allows Stripe to authentificate our API requests with our key
const stripeSecretKey = nodeEnv ? process.env.REACT_APP_stripe_dev_secret_key : process.env.REACT_APP_stripe_prod_secret_key;
const stripe = require('stripe')(stripeSecretKey);

//* To override the API version, provide the apiVersion option:
//*  Before upgrading your API version in the Dashboard, review both the API changelog and the library changelog.
/*
const stripe = require('stripe')(stripeSecretKey, {
  apiVersion: '2020-08-27',
});
*/

//* After creating a Checkout Session, redirect your customer to the URL returned in the response.
//* Add an endpoint on your server that creates a Checkout Session. A Checkout Session controls what your customer sees 
//* in the Stripe-hosted payment page such as line items, the order amount and currency, and acceptable payment methods.

const createCheckoutSession = async (req, res) => {
    
    const session = await stripe.checkout.sessions.create({

    //* Prefill customer data
    //* Use customer_email to prefill the customer’s email address in the email input field. You can also pass a
    //*  Customer ID to customer field to prefill the email address field with the email stored on the Customer.
    //* customer_email: 'customer@example.com',
        
    //* Pick a submit button // Configure the copy displayed on the Checkout submit button by setting the submit_type. There are four different submit types.
        submit_type: 'donate',
        
        /*
        Collect billing and shipping details
        Use billing_address_collection and shipping_address_collection to collect your customer’s address. 
        shipping_address_collection requires a list of allowed_countries. Checkout displays the list of allowed
        countries in a dropdown on the page.
        */
            
        billing_address_collection: 'auto',
        shipping_address_collection: {
            allowed_countries: ['US', 'CA', 'LV'],
        },

        /*
        Define a product to sell
        Always keep sensitive information about your product inventory, like price and availability, on your server 
        to prevent customer manipulation from the client. Define product information when you create the Checkout
        Session using predefined price IDs or on the fly with price_data.
        */
        /*
            line_items: [
                {
                    price: 'price_1JsxdVBSHV1ZLiWD7n4PcKf9',
                    quantity: 1,
                },
            ],
        */
        //* Provide the exact Price ID (e.g. pr_1234) of the product you want to sell
/*
            line_items: [
                cartItems.map(item => {
                    return {
                        price: item.price,
                        quantity: item.quantity,
                    }
                })
            ],
*/
            line_items: req.body.items,
            
            /*
            req.body.items.map(item => {
                return {
                    price: item.id,
                    quantity: item.qty,
                },
            },
            */

        //* When you pass multiple payment methods, Checkout dynamically displays them to prioritize what’s most 
        //* relevant to the customer. Apple Pay and Google Pay are included automatically when you include card in 
        //* payment_method_types.
        //* Apple Pay and Google Pay are enabled by default and automatically appear in Checkout when a customer 
        //* uses a supported device and has saved at least one card in their digital wallet. 
            payment_method_types: [
                'card',
            ],

        //* Choose the mode
        //* Checkout has three modes: payment, subscription, or setup. Use payment mode for one-time purchases.
        //*  Learn more about subscription and setup modes in the docs.
            mode: 'payment',

        //* Supply success and cancel URLs
        //* Specify URLs for success and cancel pages—make sure they are publicly accessible so Stripe can redirect 
        //* customers to them. You can also handle both the success and canceled states with the same URL.
            success_url: `${YOUR_DOMAIN}/stripe/stripe-success.html`, //! Change
            cancel_url: `${YOUR_DOMAIN}/stripe/stripe-cancel.html`, //! Change
        //* Activate Stripe Tax to monitor your tax obligations, automatically collect tax, and access the reports you need to file returns.
        //* automatic_tax: {enabled: true},
    });

    //* Redirect to Checkout
    //* After creating the session, redirect your customer to the Checkout page’s URL returned in the response.
  
    res.redirect(303, session.url);

};

module.exports = createCheckoutSession;

server.js(Express服务端核心配置代码)

const express = require('express');
const helmet = require('helmet'); 
const cors = require('cors'); 
const path = require('path'); // Allows to access files through the server in our filesystem

/**
**  ------------- GENERAL SETUP -------------
*/

// Provides access to variables from the .env file by using process.env.REACT_APP_variable_name
    require('dotenv').config();

    const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';
    const devPort = process.env.REACT_APP_server_dev_port;
    const prodPort = process.env.REACT_APP_server_prod_port;

//* Creates the Express server instance as "app" 
    const app = express();

//* MIDDLEWARE
// Called BETWEEN processing the Request and sending the Response in your application method.
    app.use(cors()); // To allow cross origin conections (Allows our React app to make HTTP requests to Express application)
    app.use(helmet()); // Sets many http headers to make them more secure
    app.use(express.static(path.join(__dirname, 'public'))); // To load static files or client files from here http://localhost:3000/images/kitten.jpg
    // Instead of using body-parser middleware, use the new Express implementation of the same thing
        app.use(express.json()); // To recognize the incoming Request Object (req.body) as a JSON Object
        app.use(express.urlencoded({ extended: false })); // To recognize the incoming Request Object as strings or arrays

/**
** -------------- SERVER ----------------
*/
       
// Determines the PORT and enables LISTENing for requests on the PORT (http://localhost:8000)

    const PORT = nodeEnv ? devPort : prodPort;
       
    app.listen(PORT, () => {
      console.debug(`Server is listening at http://localhost:${PORT}`);
    });
  
/**
** ------- ROUTES / ENDPOINTS ---------
*/

// Go to /test to make sure the basic API functioning is working properly
    app.get('/test', (req, res) => {
        res.status(200).send('The Basic API endpoints are working.')
    });

// Imports all of the routes from ./routes/index.js
    app.use(require('./routes/allRoutes'));
更新1#

浏览器请求控制台输出如下:

Request URL: http://localhost:3000/cart
Request Method: POST
Status Code: 404 Not Found
Remote Address: 127.0.0.1:3000
Referrer Policy: strict-origin-when-cross-origin
access-control-allow-origin: *
connection: close
content-length: 144
content-security-policy: default-src 'none'
content-type: text/html; charset=utf-8
date: Fri, 26 Nov 2021 05:25:35 GMT
expect-ct: max-age=0
referrer-policy: no-referrer
strict-transport-security: max-age=15552000; includeSubDomains
Vary: Accept-Encoding
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
X-Powered-By: Express
x-xss-protection: 0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: lv-LV,lv;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
DNT: 1
Host: localhost:3000
Origin: http://localhost:3000
Referer: http://localhost:3000/cart
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1
更新2#

目前已取得部分进展:将表单代码从:

<form method='POST' action={goToCheckout}>
     <input type='submit' value='Go To Checkout' />
</form>

修改为:

<form type="button" onSubmit={goToCheckout}>
      <button>
          Go To Checkout
      </button>
</form>

后可成功触发goToCheckout函数,但点击按钮后仍会重定向回购物车页,URL从http://localhost:3000/cart变为http://localhost:3000/cart?,添加event.preventDefault()也无效果,fetch请求未正常执行跳转至Stripe结账页。

更新3#

通过Postman访问同一路由可正常获取Stripe结账链接,跳转至测试环境支付页,说明后端路由本身运行正常,目前仅需解决表单提交导致页面刷新、fetch请求未正常执行的问题,也可提供无需form实现触发请求的方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 14:54:02