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

Next.js集成Clerk Webhook失败求助:已部署至Vercel

Clerk Webhook推送失败排查(Next.js + Clerk + MongoDB)

我使用Next.js结合Clerk实现用户认证功能,当前配置的Clerk Webhook消息推送失败。已将应用部署至Vercel,并将部署后的应用URL设置为Clerk Webhook的端点。需求是将认证用户的数据保存至MongoDB数据库,恳请协助排查问题。相关代码及配置如下:


route.js

import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
import { createUser, deleteUser, updateUser } from '@/lib/actions/user.actions';
import { clerkClient } from '@clerk/nextjs';
import { NextResponse } from 'next/server';

export async function POST(req: Request) {

  // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
  const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
    throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local');
  }

  // Get the headers
  const headerPayload = headers();
  const svix_id = headerPayload.get("svix-id");
  const svix_timestamp = headerPayload.get("svix-timestamp");
  const svix_signature = headerPayload.get("svix-signature");

  // If there are no headers, error out
  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Error occurred -- no svix headers', {
      status: 400
    });
  }

  // Get the body
  const payload = await req.json();
  const body = JSON.stringify(payload);

  // Create a new Svix instance with your secret.
  const wh = new Webhook(WEBHOOK_SECRET);

  let evt: WebhookEvent;

  // Verify the payload with the headers
  try {
    evt = wh.verify(body, {
      "svix-id": svix_id,
      "svix-timestamp": svix_timestamp,
      "svix-signature": svix_signature,
    }) as WebhookEvent;
  } catch (err) {
    console.error('Error verifying webhook:', err);
    return new Response('Error occurred', {
      status: 400
    });
  }

  // Get the ID and type
  const { id } = evt.data;
  const eventType = evt.type;

  if (eventType === 'user.created') {
    const { id, email_addresses, image_url, first_name, last_name, username } = evt.data;

    const user = {
      clerkId: id,
      email: email_addresses[0].email_address,
      username: username!,
      firstName: first_name,
      lastName: last_name,
      photo: image_url,
    };

    const newUser = await createUser(user);

    if (newUser) {
      await clerkClient.users.updateUserMetadata(id, {
        publicMetadata: {
          userId: newUser._id
        }
      });
    }

    return NextResponse.json({ message: 'OK', user: newUser });
  }

  if (eventType === 'user.updated') {
    const { id, image_url, first_name, last_name, username } = evt.data;

    const user = {
      firstName: first_name,
      lastName: last_name,
      username: username!,
      photo: image_url,
    };

    const updatedUser = await updateUser(id, user);

    return NextResponse.json({ message: 'OK', user: updatedUser });
  }

  if (eventType === 'user.deleted') {
    const { id } = evt.data;

    const deletedUser = await deleteUser(id!);

    return NextResponse.json({ message: 'OK', user: deletedUser });
  }

  return new Response('', { status: 200 });
}

middleware.js

import { authMiddleware } from "@clerk/nextjs";
 
export default authMiddleware({
  publicRoutes: [
    '/',
    '/events/:id',
    '/api/webhook',
    '/api/webhook/stripe',
    '/api/uploadthing'
  ],
  ignoredRoutes: [
    '/api/webhook/clerk',
    '/api/webhook/stripe',
    '/api/uploadthing'
  ]
});
 
export const config = {
  matcher: ['/((?!.+\.[\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
};

文件夹结构

文件夹结构

Clerk控制台配置

Clerk控制台配置


排查步骤

  • 路由路径一致性检查:
    确认Webhook路由的实际路径与middleware.js中的配置匹配。如果你的Webhook路由文件位于app/api/webhook/clerk/route.js,路径为/api/webhook/clerk,需确保该路径被加入ignoredRoutes(避免Clerk中间件拦截验证),同时可同步添加到publicRoutes做双重保障。

  • WEBHOOK_SECRET环境变量验证:

    1. 核对Clerk控制台Webhook页面的Secret值,确保Vercel环境变量中WEBHOOK_SECRET与该值完全一致(无空格、大小写错误)。
    2. 部署到Vercel时手动确认环境变量已同步,本地测试时.env.local也要配置相同值。
  • Svix签名验证修复:
    当前代码先解析req.json()再stringify,可能导致原始请求体格式(如空格、字段顺序)变化,触发Svix验证失败。改为直接读取原始请求体:

    // 替换原有的payload和body获取逻辑
    const body = await req.text();
    const payload = JSON.parse(body);
    

    用原始字符串进行签名验证,避免格式差异。

  • 事件订阅确认:
    查看Clerk控制台的Webhook配置,确认已订阅user.created、user.updated、user.deleted三个事件类型,未订阅的事件不会触发推送。

  • MongoDB操作错误捕获:
    在createUser/updateUser/deleteUser函数中添加错误日志,或在route.js中包裹try-catch:

    // 以user.created为例
    try {
      const newUser = await createUser(user);
      if (newUser) {
        await clerkClient.users.updateUserMetadata(id, {
          publicMetadata: { userId: newUser._id }
        });
      }
    } catch (dbErr) {
      console.error('MongoDB操作失败:', dbErr);
      return new Response('数据库操作失败', { status: 500 });
    }
    

    排查数据库连接、字段验证等问题。

  • Clerk Client权限检查:
    确认Clerk API密钥拥有修改用户元数据的权限,在Clerk控制台的API Keys页面查看权限范围。

  • Vercel日志排查:
    登录Vercel控制台查看应用日志,定位Webhook请求的具体错误(如400错误对应签名验证失败,500错误对应代码/数据库问题)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 09:07:38