Vercel+Supabase+React JS环境下Stripe Webhook 405错误排查求助
React+Stripe Webhook部署到Vercel后返回HTTP 405错误
我在React+Vite项目中集成了Stripe计费功能,提供免费增值模式,用户可在应用内跳转至Stripe结账页面升级付费套餐,通过instance_id关联并更新Supabase数据库的套餐数据来控制功能权限。本地运行一切正常,但部署到Vercel后,/api/stripe-webhook接口无法正常工作,Stripe控制台显示返回HTTP 405(方法不允许)状态码。
Webhook代码
const { createClient } = require('@supabase/supabase-js') const stripe = require('stripe')(process.env.VITE_STRIPE_SECRET_KEY_TEST) const supabaseUrl = process.env.VITE_SUPABASE_URL const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY const service_role_key = process.env.VITE_SUPABASE_SERVICE_KEY const supabase = createClient(supabaseUrl, supabaseAnonKey) const supabaseAdmin = createClient(supabaseUrl, service_role_key, { auth: { autoRefreshToken: true, persistSession: true, supabaseClient: supabase, }, }) const signingSecretTest = process.env.VITE_STRIPE_WEBHOOK_TEST module.exports = async (req, res) => { try { const rawBody = req.body.toString('utf8') const sigHeader = req.headers['stripe-signature'] const event = stripe.webhooks.constructEvent( rawBody, sigHeader, signingSecretTest, ) if (event.type === 'checkout.session.completed') { const session = event.data.object console.log(session) if (session?.payment_status === 'paid' && session?.client_reference_id) { const updateData = { last_payment_amount: session?.amount_total, last_payment_date: session?.created, plan_status: 'active', instance_id: session?.client_reference_id, plan_name: 'standard', price_per_seat:15 } const { error } = await supabaseAdmin .from('settings') .update(updateData) .eq('instance_id', session?.client_reference_id) if (error) { console.error('Error updating settings:', error) } } } return new Response('Webhook received successfully', { status: 200, headers: { 'Access-Control-Allow-Origin': '*', }, }) } catch (err) { console.error('Webhook error:', err) res.status(400).send(`Webhook Error: ${err.message}`) } }
已尝试的操作
- 将
stripe-webhook.js放置在/api/stripe-webhook.js路径下 - 在vercel.json中添加路由配置
- 确认Vercel环境变量配置正确(使用测试密钥)
- 本地测试功能正常
- Postman测试接口仍返回405错误
- 尝试修改Supabase的settings表权限,无效果后恢复
vercel.json配置
{ "routes": [ { "src": "/[^.]+", "dest": "/", "status": 200 }, { "src": "/api/stripe-webhook", "methods": ["POST", "OPTIONS"], "headers": { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type" }, "dest": "/api/stripe-webhook.js" } ] }
vite.config配置
import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' export default defineConfig({ define: { 'process.env.VITE_STRIPE_API_KEY_PUBLISHABLE_TEST': JSON.stringify( process.env.VITE_STRIPE_API_KEY_PUBLISHABLE_TEST, ), }, plugins: [react()], base: '/', test: { environment: 'jsdom', }, build: { outDir: 'dist', assetsDir: 'assets', }, server: { watch: { usePolling: true, }, proxy: { '/api': { target: 'your-url-here', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '/api/stripe-webhook'), }, }, }, })
解决方案建议
1. 调整vercel.json路由顺序
Vercel路由匹配是从上到下优先匹配,当前第一个路由会捕获所有非静态文件请求(包括/api/stripe-webhook),导致API路由无法被正确解析。需把API路由移到最前面:
{ "routes": [ { "src": "/api/stripe-webhook", "methods": ["POST", "OPTIONS"], "headers": { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type" }, "dest": "/api/stripe-webhook.js" }, { "src": "/[^.]+", "dest": "/", "status": 200 } ] }
2. 修正Webhook响应方式
Vercel API路由使用Express风格的req/res对象,混用Fetch API的Response会导致响应异常,统一使用res对象返回:
// 替换原return new Response(...)代码 res.status(200).setHeader('Access-Control-Allow-Origin', '*').send('Webhook received successfully');
3. 增加请求方法校验
在Webhook代码开头添加校验,拒绝非POST请求:
module.exports = async (req, res) => { // 新增:仅允许POST请求 if (req.method !== 'POST') { return res.status(405).send('Method Not Allowed'); } try { // 原有业务代码... } catch (err) { // 原有错误处理... } }
4. 规范后端环境变量
Vite的VITE_前缀变量用于前端注入,后端API应使用不带前缀的变量(如STRIPE_SECRET_KEY_TEST),避免潜在的暴露或读取问题。在Vercel控制台重新配置变量后,修改Webhook代码中的变量名:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY_TEST) const supabaseUrl = process.env.SUPABASE_URL const supabaseAnonKey = process.env.SUPABASE_ANON_KEY const service_role_key = process.env.SUPABASE_SERVICE_KEY const signingSecretTest = process.env.STRIPE_WEBHOOK_TEST
5. 优化Supabase Admin客户端配置
后端使用service_role_key时无需维持用户会话,关闭自动刷新和会话持久化:
const supabaseAdmin = createClient(supabaseUrl, service_role_key, { auth: { autoRefreshToken: false, persistSession: false, }, })
6. 确保获取原始请求体用于签名验证
Stripe要求使用原始请求体进行签名验证,Vercel默认会解析JSON请求体,需禁用自动解析并手动读取原始body:
// 禁用自动解析 export const config = { api: { bodyParser: false, }, } module.exports = async (req, res) => { // 手动读取原始body const rawBody = await new Promise((resolve) => { let chunks = [] req.on('data', (chunk) => chunks.push(chunk)) req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) }) // 后续签名验证及业务代码... }
内容的提问来源于stack exchange,提问作者Jessie Mills
相关产品推荐
相关产品推荐

