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

Next.js处理Clerk Webhook时遭遇401 Unauthorized错误求助

问题

使用Clerk Webhook同步数据到后端,本地配置正常,但部署到Vercel后,Clerk请求返回401错误。已尝试调整ignoredRoutes、更新WEBHOOK_SECRET并在Vercel配置环境变量后重新部署,问题仍未解决。

相关代码

app/api/webhook/route.ts

/* eslint-disable camelcase */
import { Webhook } from "svix";
import { headers } from "next/headers";
import { WebhookEvent } from "@clerk/nextjs/server";
import { createUser, updateUser, deleteUser } 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
  // TODO: add your webhook secret to .env.loca
  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 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,
    });
  }

  // Get the event type
  const eventType = evt.type;

  console.log({ eventType });

  // Handle the event
  if (eventType === "user.created") {
    const { id, email_addresses, image_url, username, first_name, last_name } = evt.data;

    // 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,
    });

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

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

    // Update a user in 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;

    // Delete a user in database
    const deletedUser = await deleteUser({ clerkId: id! });

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

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

middleware.ts

import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  publicRoutes: [
    "/",
    "/api/webhook",
    "question/:id",
    "/tags",
    "/tags/:id",
    "/profile/:id",
    "/community",
    "/jops",
  ],
  ignoredRoutes: ["/api/webhook", "/api/chatgpt"],
});

export const config = {
  matcher: ["/((?!.+\.[\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
解决方案
  • 确认ignoredRoutes路径匹配规则:Clerk的ignoredRoutes会完全跳过认证检查,优先级高于publicRoutes。检查路径是否完全匹配,排除大小写、路径重写等问题;可尝试将/api/webhook调整为/api/webhook/:*(即使无子女路径,确保覆盖所有子请求)。
  • 核对Vercel环境变量:
    • 确保Vercel生产环境中配置的WEBHOOK_SECRET与Clerk Dashboard生成的密钥完全一致,无多余空格或换行。
    • 检查变量是否已同步到生产环境,而非仅预览环境。
  • 修正Webhook验证逻辑:
    • 替换const payload = await req.json(); const body = JSON.stringify(payload);为const body = await req.text();,避免因二次序列化导致的签名验证失败(Clerk发送的原始body格式可能与重新序列化的不一致)。
  • 检查Clerk Webhook配置:
    • 确认Clerk中Webhook的端点URL为https://你的Vercel域名/api/webhook,无拼写错误。
    • 确保已勾选需要触发的事件类型(user.created/updated/deleted),且Webhook处于启用状态。
  • 排查中间件匹配规则:
    • 当前matcher中的/(api|trpc)(.*)会匹配所有API路径,需确认ignoredRoutes的/api/webhook能被正确识别。可暂时注释matcher中的API规则,测试是否为中间件拦截导致401。
  • 查看Vercel详细日志:
    • 在Vercel函数日志中查看完整请求头和中间件处理日志,确认401来源是Clerk中间件拦截,还是Webhook验证逻辑异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 01:56:16