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生成的密钥完全一致,无多余空格或换行。 - 检查变量是否已同步到生产环境,而非仅预览环境。
- 确保Vercel生产环境中配置的
- 修正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处于启用状态。
- 确认Clerk中Webhook的端点URL为
- 排查中间件匹配规则:
- 当前
matcher中的/(api|trpc)(.*)会匹配所有API路径,需确认ignoredRoutes的/api/webhook能被正确识别。可暂时注释matcher中的API规则,测试是否为中间件拦截导致401。
- 当前
- 查看Vercel详细日志:
- 在Vercel函数日志中查看完整请求头和中间件处理日志,确认401来源是Clerk中间件拦截,还是Webhook验证逻辑异常。
内容的提问来源于stack exchange,提问作者Kekoo
相关产品推荐
相关产品推荐

