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

Stripe生产环境成功重定向超时问题排查求助

Stripe支付后重定向超时及Webhook触发失败问题

问题现象

  • 测试环境运行正常,生产环境中用户提交支付信息后,Stripe结账页面加载图标持续转动,无法重定向回应用
  • Stripe控制台显示客户账户、订阅已成功创建,但Webhook触发失败,错误提示为Timed out connecting to remote host
  • 重定向失败会导致Webhook无法触发,进而无法为用户配置对应订阅等级的核心功能

怀疑点

推测createCheckoutSession中的success_url配置可能存在阻塞:

success_url: `${webUrl}/users/${req.user._id}/activations/?session_id={CHECKOUT_SESSION_ID}`,

创建结账会话代码

module.exports.createCheckoutSession = async (req, res) => {
    const { currency, selectedPriceId } = req.body;
    const user = await User.findById(req.user.id);

    const cloakedPriceId = pricing[selectedPriceId].priceId;

    if (req.isAuthenticated()) {
        if (((!req.user.stripeId) || (req.user.stripeId.length < 1))) {
            const new_stripe_customer = await stripe.customers.create({
                email: req.user.email,
                    metadata: {
                    user_id: req.user.id.toString(),
                },
            });

            let update = {};
            await User.findByIdAndUpdate(req.user._id, update);

            if (!req.user.stripe_customer) {
                req.user.stripe_customer = {};
            } else {
                req.user.stripe_customer[currency] = new_stripe_customer.id;
            }
            user.stripeId = new_stripe_customer.id;
            await user.save();
            try {
                const session = await stripe.checkout.sessions.create({
                    mode: "subscription",
                    customer: req.user.stripe_customer[currency],
                    customer_email: req.user.email,
                    line_items: [
                        {
                            price: cloakedPriceId,
                            quantity: 1,
                        },
                    ],
                    success_url: `${webUrl}/users/${req.user._id}/activations/?session_id={CHECKOUT_SESSION_ID}`,
                    cancel_url: `${webUrl}/canceled.html`,
                });
                return res.redirect(303, session.url);

            } catch (e) {
                res.status(400);
                return res.send({
                    error: {
                        message: e.message,
                    }
                });
            }
        } else {
            try {
                const session = await stripe.checkout.sessions.create({
                    mode: "subscription",
                    customer: req.user.stripeId,
                    line_items: [
                        {
                            price: cloakedPriceId,
                            quantity: 1,
                        },
                    ],
                    metadata: {
                        user_id: req.user.id.toString(),
                    },
                    success_url: `${webUrl}/users/${req.user._id}/activations/?session_id={CHECKOUT_SESSION_ID}`,
                    cancel_url: `${webUrl}/canceled.html`,
                });
                return res.redirect(303, session.url);

            } catch (e) {
                res.status(400);
                return res.send({
                    error: {
                        message: e.message,
                    }
                });
            }
        }
    }
    req.flash('error', "You must login before changing billing");
    res.render('/login');
};

Webhook代码

module.exports.stripeWebhook = async (req, res) => {
    let data;
// Check if webhook signing is configured.
    const webhookSecret = stripeWebhookSecret;
    if (webhookSecret) {
        // Retrieve the event by verifying the signature using the raw body and secret.
        let event;
        let signature = req.headers["stripe-signature"];
        console.log("signature ", signature);

        try {
            event = stripe.webhooks.constructEvent(
                req.body,
                signature,
                webhookSecret
            );
        } catch (err) {
            console.log(`⚠️  Webhook signature verification failed.`);
            return res.sendStatus(400);
        }
        // Extract the object from the event.
        data = event.data;
        eventType = event.type;
    } else {
        // Webhook signing is recommended, but if the secret is not configured in `config.js`,
        // retrieve the event data directly from the request body.
        data = req.body.data;
        eventType = req.body.type;
        console.log("event type ", eventType);
    }

    switch (eventType) {
        case 'payment_intent.succeeded': {
            // add name to stripe customer in stripe dashboard
            const paymentIntent = data.object;
            // console.log("payment intent ", paymentIntent);
            const customer = paymentIntent.customer;
            // console.log("customer ", customer);
            const name = paymentIntent.metadata.name;
            // console.log("name ", name);

            // use stripe customer id to check customer subscription status
            const customerSubscription = await stripe.customers.retrieve(
                customer,
                {
                    expand: ['subscriptions'],
                }
            );
            console.log("customer subscription ", customerSubscription);
            // const user = await User.findById(customerSubscription.metadata.user_id);
            // FIND USER BY email
            const user = await User.findOne({ email: customerSubscription.email });
            console.log("user ", user);

            // get product name from stripe product api
            const product = await stripe.products.retrieve(
                customerSubscription.subscriptions.data[0].items.data[0].price.product
            );
            console.log("product ", product);
            const productCap = product.name;
            productName = productCap.toLowerCase();
            const subscriptionDetails = customerSubscription.subscriptions.data[0].status;
            console.log("product name ", productName);
            user.product = productName;
            user.subscription.active = subscriptionDetails;
            await user.save();

            break;
        }
        case 'checkout.session.completed':
            // Payment is successful and the subscription is created.
            // You should provision the subscription and save the customer ID to your database.
            break;
        case 'invoice.paid':
            // Continue to provision the subscription as payments continue to be made.
            // Store the status in your database and check when a user accesses your service.
            // This approach helps you avoid hitting rate limits.
            break;
        case 'invoice.payment_failed': {
            // The payment failed or the customer does not have a valid payment method.
            // The subscription becomes past_due. Notify your customer and send them to the
            // customer portal to update their payment information.
        }
            break;
        case 'payment_intent.failed': {
            // The payment failed or the customer does not have a valid payment method.
            // The subscription becomes past_due. Notify your customer and send them to the
            // customer portal to update their payment information.
        }
            break;
        default:
        // Unhandled event type
    }
    res.sendStatus(200);
};

客户端代码

<title>Checkout</title>

<div class="row">
    <h2 class="text-center">Confirm plan selection</h2>
    <p>Confirm plan before entering billing information</p>
</div>
<form action="/users/stripe/create-checkout-session" method="POST">

<div class="row row-cols-1 row-cols-md-3 g-4">
        <div class="col">
            <div class="card mb-4 rounded-3 shadow-sm">
                <div class="card-header py-3">
                    <h4 class="my-0 fy-normal">Basic</h4>
                </div>
                <div class="card-body">
                    <h1 class="card-title pricing-card-title">$29.99
                        <small class="text-muted fw-light">/mo</small>
                    </h1>
                    <ul class="list-unstyled mt-3 mb-4">
                        <li class="p-1">2 QR links</li>
                        <li class="p-1">Up to 35 participants per activation</li>
                        <li class="p-1">5gb storge</li>
                        <li class="p-1">Cancel or freeze whenever</li>
                    </ul>
                    <button class="button btn-primary btn-lg" type="hidden" id="selectedPriceId"
                        name="selectedPriceId" value="basic">Confirm Basic</button>
                </div>
            </div>
        </div>
        <div class="col">
            <div class="card mb-4 rounded-3 shadow-sm">
                <div class="card-header py-3">
                    <h4 class="my-0 fy-normal">Scaled</h4>
                </div>
                <div class="card-body">
                    <h1 class="card-title pricing-card-title">$99.99
                        <small class="text-muted fw-light">/mo</small>
                    </h1>
                    <ul class="list-unstyled mt-3 mb-4">
                        <li class="p-1">7 QR links</li>
                        <li class="p-1">Up to 100 participants per activation</li>
                        <li class="p-1">10gb storge</li>
                        <li class="p-1">Cancel or freeze whenever</li>
                    </ul>
                    <button class="button btn-primary btn-lg" type="hidden" id="selectedPrice" name="selectedPriceId" value="scaled">Confirm Scaled</button>
                </div>
            </div>
        </div>
        <div class="col">
            <div class="card mb-4 rounded-3 shadow-sm">
                <div class="card-header py-3">
                    <h4 class="my-0 fy-normal">Enterprise</h4>
                </div>
                <div class="card-body">
                    <h1 class="card-title pricing-card-title">$199.99
                        <small class="text-muted fw-light">/mo</small>
                    </h1>
                    <ul class="list-unstyled mt-3 mb-4">
                        <li class="p-1">10 QR links</li>
                        <li class="p-1">Up to 1000 participants per activation</li>
                        <li class="p-1">Unlimited storge</li>
                        <li class="p-1">Cancel or freeze whenever</li>
                    </ul>
                    <button class="button btn-primary btn-lg" type="hidden" id="selectedPriceId"
                    name="selectedPriceId" value="enterprise">Confirm Enterprise</button>
                </div>
            </div>
        </div>
</div>       
</form>

问题原因分析及解决方案

1. Webhook超时是核心问题

Stripe要求Webhook接收端在10秒内返回响应,但你的Webhook代码中存在多个串行异步操作:调用Stripe API查询客户、订阅、产品,再查询并更新数据库用户,这些操作叠加后很容易超过10秒,导致Stripe判定超时。生产环境的网络延迟、数据库查询速度慢会进一步加剧这个问题,测试环境因数据量小、网络稳定未触发超时。

用户看到结账页面持续加载,是因为Stripe在Webhook超时后会尝试重试,同时延迟重定向逻辑——本质上success_url本身没有阻塞,而是Webhook失败影响了整体流程。

2. 具体修复步骤

(1)优化Webhook响应逻辑

先给Stripe返回200状态码,再异步处理业务逻辑。可以用消息队列(如BullMQ)把耗时操作放到后台执行,避免阻塞请求:

module.exports.stripeWebhook = async (req, res) => {
    let event;
    const webhookSecret = stripeWebhookSecret;
    try {
        event = stripe.webhooks.constructEvent(
            req.body,
            req.headers["stripe-signature"],
            webhookSecret
        );
    } catch (err) {
        console.log(`⚠️  Webhook签名验证失败: ${err.message}`);
        return res.sendStatus(400);
    }

    // 立即返回响应,告知Stripe已接收事件
    res.sendStatus(200);

    // 后台异步处理业务逻辑
    processStripeEvent(event).catch(err => {
        console.error(`处理Stripe事件失败: ${err.message}`);
        // 添加失败重试逻辑
    });
};

// 单独的异步处理函数
async function processStripeEvent(event) {
    const data = event.data;
    const eventType = event.type;

    switch (eventType) {
        case 'checkout.session.completed': {
            const session = data.object;
            const customer = await stripe.customers.retrieve(session.customer, { expand: ['subscriptions'] });
            const user = await User.findOne({ email: customer.email });
            if (!user) return;

            const product = await stripe.products.retrieve(
                customer.subscriptions.data[0].items.data[0].price.product
            );
            user.product = product.name.toLowerCase();
            user.subscription.active = customer.subscriptions.data[0].status;
            await user.save();
            break;
        }
        // 其他事件处理...
    }
}

(2)修正createCheckoutSession中的无效代码

删除无意义的User.findByIdAndUpdate调用(let update = {}; await User.findByIdAndUpdate(req.user._id, update);),避免浪费请求时间;同时统一用户对象更新逻辑,避免数据不一致。

(3)验证success_url可达性

确保生产环境webUrl配置的域名公网可访问,且/users/${req.user._id}/activations/路由无权限拦截或响应慢问题,可直接在浏览器访问测试。

(4)优先使用checkout.session.completed事件

订阅场景下,checkout.session.completed事件在结账完成、订阅创建成功后立即触发,比payment_intent.succeeded更贴合业务逻辑,能避免重复处理。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 17:25:27