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

Stripe支付后触发405错误,Prisma数据库未更新求助

Stripe Webhook返回405 Method Not Allowed错误(Vercel部署)

我的AI SaaS应用已集成Replicate数据交互,接入Stripe支付系统实现付费功能,创建了/pages/api/stripe-webhook端点,但Stripe回调请求被Vercel服务器拒绝,返回405 'Method Not Allowed'错误,导致支付成功后无法更新Prisma数据库中的用户积分及购买记录。


Webhook端点代码 (/pages/api/stripe-webhook.ts)

import { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../lib/prismadb";
import Stripe from "stripe";
import { buffer } from "micro";
import Cors from "micro-cors";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2022-11-15",
});

export const config = {
  api: {
    bodyParser: false,
  },
};

const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET || "";

const cors = Cors({
  allowMethods: ["POST", "HEAD"],
});

const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "POST") {
    const buf = await buffer(req);
    const sig = req.headers["stripe-signature"]!;

    let event: Stripe.Event;

    try {
      event = stripe.webhooks.constructEvent(
        buf.toString(),
        sig,
        webhookSecret
      );
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : "Unknown error";
      if (err! instanceof Error) console.log(err);
      console.log(`❌ Error message: ${errorMessage}`);
      res.status(400).send(`Webhook Error: ${errorMessage}`);
      return;
    }

    console.log("✅ Success:", event.id);

    if (
      event.type === "payment_intent.succeeded" ||
      event.type === "checkout.session.completed"
    ) {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      console.log(`💰 PaymentIntent: ${JSON.stringify(paymentIntent)}`);

      // @ts-ignore
      const userEmail = paymentIntent.customer_details.email;
      let creditAmount = 0;

      // @ts-ignore
      switch (paymentIntent.amount_subtotal) {
        case 500:
        case 1000:
          creditAmount = 20;
          break;
        case 1900:
        case 3000:
          creditAmount = 100;
          break;
        case 3500:
        case 5000:
          creditAmount = 250;
          break;
        case 7000:
        case 10000:
          creditAmount = 750;
          break;
      }
      await prisma.user.update({
        where: {
          email: userEmail,
        },
        data: {
          credits: {
            increment: creditAmount,
          },
        },
      });

      await prisma.purchase.create({
        data: {
          creditAmount: creditAmount,
          user: {
            connect: {
              email: userEmail,
            },
          },
        },
      });
    } else if (event.type === "payment_intent.payment_failed") {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      console.log(
        `❌ Payment failed: ${paymentIntent.last_payment_error?.message}`
      );
    } else if (event.type === "charge.succeeded") {
      const charge = event.data.object as Stripe.Charge;
      console.log(`💵 Charge id: ${charge.id}`);
    } else {
      console.warn(`🤷‍♀️ Unhandled event type: ${event.type}`);
    }

    res.json({ received: true });
  } else {
    res.setHeader("Allow", "POST");
    res.status(405).end("Method Not Allowed");
  }
};

export default cors(webhookHandler as any);

Prisma Schema

datasource db {
  provider          = "postgresql"
  url               = env("DATABASE_URL")
  directUrl         = env("DIRECT_URL")
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String     @id @default(cuid())
  name          String?
  email         String?    @unique
  emailVerified DateTime?
  image         String?
  credits       Int        @default(1)
  location      String?
  accounts      Account[]
  sessions      Session[]
  rooms         Room[]
  purchases     Purchase[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

model Room {
  id          String   @id @default(cuid())
  replicateId String
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  inputImage  String
  outputImage String
  prompt      String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@unique([replicateId, userId])
}

model Purchase {
  id           String   @id @default(cuid())
  userId       String
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  creditAmount Int
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
}

解决方法

1. 移除不必要的CORS配置

Stripe webhook请求由服务器直接发起,不存在浏览器跨域问题,当前的micro-cors配置可能与Vercel的路由规则冲突,导致请求被拦截:

  • 删除CORS相关导入与包装代码
  • 将export default cors(webhookHandler as any);改为export default webhookHandler;

2. 修正事件类型转换错误

checkout.session.completed事件的对象是Checkout.Session,而非PaymentIntent,类型强制转换会导致逻辑异常,需分开处理:

if (event.type === "payment_intent.succeeded") {
  const paymentIntent = event.data.object as Stripe.PaymentIntent;
  const userEmail = paymentIntent.customer_details?.email;
  // 原有积分计算与数据库操作逻辑
} else if (event.type === "checkout.session.completed") {
  const session = event.data.object as Stripe.Checkout.Session;
  const userEmail = session.customer_details?.email;
  const amountSubtotal = session.amount_subtotal;
  // 复用积分计算逻辑
  let creditAmount = 0;
  switch (amountSubtotal) {
    case 500: case 1000: creditAmount = 20; break;
    case 1900: case 3000: creditAmount = 100; break;
    case 3500: case 5000: creditAmount = 250; break;
    case 7000: case 10000: creditAmount = 750; break;
  }
  // 数据库操作逻辑
}

3. 验证环境变量与Webhook配置

  • 确认Vercel环境变量中STRIPE_WEBHOOK_SECRET为Stripe Dashboard生成的签名密钥(而非测试密钥)
  • 检查Stripe Dashboard中配置的Webhook URL与Vercel部署后的URL完全一致(必须为HTTPS协议)

4. 增加数据库操作容错

添加错误捕获,避免数据库操作失败导致Webhook返回错误(Stripe会自动重试失败的Webhook请求):

try {
  await prisma.user.update({
    where: { email: userEmail },
    data: { credits: { increment: creditAmount } },
  });
  await prisma.purchase.create({
    data: { creditAmount, user: { connect: { email: userEmail } } },
  });
} catch (err) {
  console.error("数据库更新失败:", err);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 13:52:06