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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 12:45:33