如何将UserId从Payment API端点传递至Webhook并解决undefined问题
解决Stripe Webhook中userUid偶尔为undefined的问题
问题分析
核心问题是首次支付后Webhook能正常获取userUid,但后续请求中userUid变为undefined,原因包括:
- Stripe触发的事件类型不同(如首次是
checkout.session.completed,后续可能是payment_intent.succeeded),不同事件的metadata存储位置不一致 - 当前代码仅从
event.data.object.metadata提取userUid,未适配多事件类型的场景 - 未对
userUid做强制校验,导致空值流入Firebase操作
解决方案
1. 全局传递userUid到Stripe关联对象
创建Checkout Session时,将userUid同步到Customer、Session和Payment Intent的metadata中,确保多事件类型下都能获取到。
2. Webhook多维度提取userUid
根据事件类型从对应位置提取userUid,当前事件对象取不到时,从关联的Customer拉取兜底。
3. 强制校验userUid非空
执行Firebase操作前必须确认userUid存在,否则直接返回错误。
4. 优化Firebase初始化
避免在API路由中重复初始化Firebase,统一封装初始化逻辑。
修改后的代码
api/stripe/stripe-payment.js
import Stripe from "stripe"; import { initializeApp } from "firebase/app"; import { NextResponse } from "next/server"; import { getDatabase, ref, set } from "firebase/database"; const firebaseConfig = { apiKey: "some", authDomain: "some", projectId: "some", storageBucket: "some", messagingSenderId: "some", appId: "some", measurementId: "some", databaseURL: "some", }; // 全局初始化Firebase与Stripe const app = initializeApp(firebaseConfig); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function POST(req) { const reqBody = await req.json(); const { amount, metaData } = reqBody; // 提前校验userUid必填 if (!metaData?.userUid) { return NextResponse.json( { error: "userUid is required" }, { status: 400 } ); } try { const customer = await stripe.customers.create({ metadata: { userUid: metaData.userUid, }, }); const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: [ { price_data: { currency: "usd", product_data: { name: "INV-" + new Date().toISOString(), }, unit_amount: amount * 100 || 100, }, quantity: 1, }, ], metadata: { userUid: metaData.userUid, customer: customer.id, }, mode: "payment", success_url: `http://localhost:3000/success`, cancel_url: `${req.headers.get("host")}`, customer: customer.id, // 让Payment Intent继承userUid payment_intent_data: { metadata: { userUid: metaData.userUid, }, }, }); const db = getDatabase(); const userRef = ref(db, `userUids/${Date.now()}`); await set(userRef, { userUid: metaData.userUid, customerID: customer.id, sessionID: session.id, timestamp: new Date().toISOString(), }); return NextResponse.json({ session }); } catch (err) { console.error("Checkout session error:", err); return NextResponse.json( { error: "Error creating checkout session" }, { status: 500 } ); } }
api/stripe/stripe-webhook.js
import Stripe from "stripe"; import { initializeApp } from "firebase/app"; import { NextResponse } from "next/server"; import { getDatabase, ref, set } from "firebase/database"; const firebaseConfig = { apiKey: "some", authDomain: "some", projectId: "some", storageBucket: "some", messagingSenderId: "some", appId: "some", measurementId: "some", databaseURL: "some", }; // 全局初始化Firebase与Stripe const app = initializeApp(firebaseConfig); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET_KEY; export async function POST(req) { const sig = req.headers.get("stripe-signature"); if (!sig) { return NextResponse.json( { error: "No stripe-signature header value was provided." }, { status: 400 } ); } let event; let userUid; try { const rawBody = await req.text(); event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret); // 根据事件类型提取userUid switch (event.type) { case "checkout.session.completed": userUid = event.data.object.metadata.userUid; break; case "payment_intent.succeeded": // 优先从Payment Intent取,取不到则从关联Customer拉取 userUid = event.data.object.metadata.userUid; if (!userUid && event.data.object.customer) { const customer = await stripe.customers.retrieve(event.data.object.customer); userUid = customer.metadata.userUid; } break; default: console.log(`Unhandled event type: ${event.type}`); } // 强制校验userUid非空 if (!userUid) { console.error("userUid not found in event:", event.type); return NextResponse.json( { error: "userUid not found in event data" }, { status: 400 } ); } } catch (err) { console.error(`Webhook error: ${err}`); return NextResponse.json({ error: "Webhook error" }, { status: 400 }); } // 保存事件到Firebase(避免覆盖已有事件) try { const db = getDatabase(); const userEventsRef = ref(db, `users/${userUid}/events/${event.id}`); await set(userEventsRef, { type: event.type, timestamp: new Date().toISOString(), eventData: event.data.object, }); console.log(`Event saved for user ${userUid}: ${event.type}`); } catch (error) { console.error("Firebase save error:", error); return NextResponse.json({ error: "Firebase error" }, { status: 500 }); } return NextResponse.json({ received: true }); }
额外注意事项
- 确保Stripe CLI转发所有需要的事件类型(如
checkout.session.completed、payment_intent.succeeded) - 前端调用
stripe-payment接口时,必须传递当前登录用户的有效userUid - 建议为Firebase的
users/{userUid}/events节点设置安全规则,限制仅对应用户读写
内容的提问来源于stack exchange,提问作者Hamza Bhai
相关产品推荐
相关产品推荐

