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

使用Webhooks时用户信息未同步至MongoDB的问题排查

问题:Clerk Webhook触发401,用户信息无法存入MongoDB

我用Next.js 14的App Router搭建网站,登录功能基于Clerk ID和Webhooks实现。需求是用户注册时自动把信息存入MongoDB,但现在用户能正常通过Clerk注册,数据却没写入数据库,Vercel日志显示请求返回401状态码。以下是相关代码:

/api/webhook/route.ts

/* eslint-disable camelcase */
import { Webhook } from "svix";
import { headers } from "next/headers";
import { WebhookEvent } from "@clerk/nextjs/server";
import { createUser, deleteUser, updateUser } from "@/lib/actions/user.action";
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.NEXT_CLERK_WEBHOOK_SECRET;
  console.log(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 occured -- 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 occured", {
      status: 400,
    });
  }

  const eventType = evt.type;

  console.log({ eventType });

  if (eventType === "user.created") {
    const { id, email_addresses, image_url, username, first_name, last_name } = 
      evt.data;
    console.log("Event data:", {
      id,
      email_addresses,
      image_url,
      username,
      first_name,
      last_name,
    });

    // Create a new user in your database
    const mongoUser = await createUser({
      clerkId: id,
      name: `${first_name}${last_name ? ` ${last_name}` : ""}`,
      username: username!,
      email: email_addresses[0].email_address,
      picture: image_url,
    });
    console.log(mongoUser);

    return NextResponse.json({ message: "OK", user: mongoUser });
  }

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

    // Create a new user in your database
    const mongoUser = await updateUser({
      clerkId: id,
      updateData: {
        name: `${first_name}${last_name ? ` ${last_name}` : ""}`,
        username: username!,
        email: email_addresses[0].email_address,
        picture: image_url,
      },
      path: `/profile/${id}`,
    });

    return NextResponse.json({ message: "OK", user: mongoUser });
  }

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

    const deletedUser = await deleteUser({
      clerkId: id!,
    });

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

  return NextResponse.json({ message: "OK" });
}

user.action.ts

"use server";

import User from "@/database/user.model";
import { connectToDatabase } from "../mongoose";
import {
  CreateUserParams,
  DeleteUserParams,
  UpdateUserParams,
} from "./shared.types";
import { revalidatePath } from "next/cache";
import Question from "@/database/question.model";

export async function getUserById(params: any) {
  try {
    connectToDatabase();

    const { userId } = params;
    const user = await User.findOne({ clerkId: userId });
    return user;
  } catch (error) {
    console.log(error);
    throw error;
  }
}

export async function createUser(userData: CreateUserParams) {
  try {
    connectToDatabase();

    const newUser = await User.create(userData);
    console.log("User created:", newUser);
    return newUser;
  } catch (error) {
    console.error("Error creating user:", error);
    throw error;
  }
}

export async function updateUser(params: UpdateUserParams) {
  try {
    connectToDatabase();

    const { clerkId, updateData, path } = params;

    await User.findOneAndUpdate({ clerkId }, updateData, {
      new: true,
    });

    revalidatePath(path);
  } catch (error) {
    console.log(error);
    throw error;
  }
}

export async function deleteUser(params: DeleteUserParams) {
  try {
    connectToDatabase();
    const { clerkId } = params;

    const user = await User.findOneAndDelete({ clerkId });
    if (!user) {
      throw new Error("User not found");
    }

    await Question.deleteMany({ author: user._id });

    const deletedUser = await User.findByIdAndDelete(user._id);
    return deletedUser;
  } catch (error) {}
}

排查与解决方案

1. 检查Webhook Secret配置

  • 确认.env/.env.local中的NEXT_CLERK_WEBHOOK_SECRET和Clerk Dashboard里Webhook的Secret完全一致,注意不要有多余空格或换行。
  • Vercel部署时要确保环境变量已正确配置,在Vercel项目的Settings -> Environment Variables中检查该变量是否存在且值正确。

2. 验证Webhook请求的访问权限

  • 401状态码通常表示身份验证失败,检查是否有Next.js中间件(middleware.ts)或路由组的权限规则拦截了/api/webhook的请求,需确保该路径允许匿名访问。
  • 确认Clerk Dashboard中配置的Webhook URL是https://你的域名/api/webhook,测试环境需用ngrok等工具暴露本地服务的公网地址,确保Clerk能正常回调。

3. 修复MongoDB连接的异步问题

  • connectToDatabase()是异步函数,当前代码中直接调用未加await,会导致数据库操作在连接建立前执行,需修改所有数据库操作函数:
    // 示例:createUser函数中修改
    await connectToDatabase();
    

4. 完善错误捕获与日志

  • 在Webhook的user.created分支添加try-catch,捕获数据库操作错误并返回明确响应:
    if (eventType === "user.created") {
      // ... 现有数据提取逻辑
      try {
        const mongoUser = await createUser({/* 用户数据 */});
        console.log(mongoUser);
        return NextResponse.json({ message: "OK", user: mongoUser });
      } catch (dbError) {
        console.error("数据库写入失败:", dbError);
        return new Response("数据库操作失败", { status: 500 });
      }
    }
    
  • 在Webhook的POST函数最外层添加全局错误捕获,避免未处理的错误导致请求异常:
    export async function POST(req: Request) {
      try {
        // 现有所有逻辑
      } catch (err) {
        console.error("Webhook全局错误:", err);
        return new Response("服务器内部错误", { status: 500 });
      }
    }
    

5. 检查Clerk Webhook的事件配置

  • 在Clerk Dashboard的Webhooks页面,确认user.created事件已被勾选,且Webhook状态为"Active"。
  • 点击Webhook的"Test"按钮,手动触发user.created事件,查看Vercel日志中的详细错误信息,定位是签名校验、数据解析还是数据库操作的问题。

6. 验证MongoDB访问权限

  • 检查MongoDB Atlas的IP白名单是否允许Vercel的IP访问,测试阶段可临时设置为0.0.0.0/0(生产环境需限制为Vercel IP段)。
  • 确认.env中的MongoDB连接字符串包含正确的用户名、密码和数据库名称,没有拼写错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 08:44:52