Nguồn lớn nhất của các handler webhook hỏng trong Next.js App Router là body thô. App Router ổn. Route handler ổn. Nhưng quy ước lấy body chưa parse đã thay đổi giữa Pages Router và App Router, và hầu hết các tích hợp Stripe / GitHub / Shopify sao chép đoạn mã từ thế giới cũ. Bài này là thế giới mới.
Quy tắc hai dòng
Trong một route handler App Router, lấy body thô bằng await request.text(). Đừng dùng request.json() trước khi xác minh chữ ký. Đừng cố tắt bodyParser như bạn làm trong Pages Router — cờ đó không còn nữa.
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text(); // raw, not parsed
const signature = (await headers()).get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err) {
return NextResponse.json(
{ error: 'invalid signature' },
{ status: 400 },
);
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
// ...
break;
}
return NextResponse.json({ received: true });
}
Ba dòng đáng chú ý: request.text() cho bạn chuỗi thô mà Stripe đã ký. headers() trong App Router là async — chú ý await. Và việc xác minh chữ ký phải xảy ra trước bất kỳ tương đương JSON.parse nào.
Thói quen Pages Router cần bỏ
Nếu bạn đã di trú từ Pages Router, bạn nhớ đã viết cái này:
// pages/api/webhooks/stripe.ts (OLD)
export const config = { api: { bodyParser: false } };
Config đó không tồn tại trong App Router. Body được đọc theo nhu cầu từ đối tượng Request. Bất cứ gì bạn đọc thứ hai đều rỗng — request.text() sau request.json() không trả gì. Chọn một, và cho webhook luôn chọn text().
Thiết lập tunnel cho dev Next.js
- Chạy
next dev(hoặcnpm run dev) — thường cổng 3000. - Ở terminal khác,
npx portpreview 3000. - Dán URL HTTPS vào cấu hình webhook của nhà cung cấp. Với Stripe: Developers → Webhooks → Add endpoint.
- Kích hoạt một sự kiện thử. Yêu cầu đến route handler của bạn với toàn bộ header nguyên vẹn.
Nếu bạn dùng next dev --turbo, cái này vẫn hoạt động. Turbopack thay đổi bundler, không phải mô hình request/response.
Edge runtime: đừng
Nếu bạn đặt export const runtime = 'edge' trên một route handler webhook, bạn sẽ mất quyền truy cập các API crypto chỉ-Node mà một số SDK nhà cung cấp dùng. SDK Node của Stripe hoạt động trên edge ở các phiên bản gần đây, nhưng xác minh chữ ký với nhà cung cấp bất kỳ thì hên xui. Giữ route webhook trên Node runtime trừ khi bạn có lý do cụ thể.
export const runtime = 'nodejs'; // explicit, safe default for webhooks
Những bẫy App Router chúng tôi liên tục gặp
Trả về sai kiểu
Nếu bạn trả new Response(JSON.stringify(...)) thay vì NextResponse.json(...), header content-type có thể không được đặt. Một số nhà cung cấp quan tâm. Dùng NextResponse.json() hoặc đặt header thủ công.
Streaming và websocket
Handler webhook là yêu cầu HTTP ngắn hạn, nhưng nếu bạn tunnel một app Next.js cũng có route WebSocket (ví dụ với socket.io trên máy chủ tùy chỉnh), đảm bảo tunnel của bạn giữ lại upgrade WebSocket. Hầu hết đều giữ; xem tài liệu.
Middleware đọc body
Nếu bạn có middleware.ts ở gốc gọi request.text() để log hoặc auth, route webhook của bạn không bao giờ thấy body. Bỏ qua đường dẫn webhook trong matcher hoặc chỉ đọc body trong các route cần.
Deploy lên Vercel không cứu bạn
Các lỗi webhook phụ thuộc vào việc parse body thô hành xử giống hệt trên Vercel và trên localhost. Khi kiểm thử tunnel cục bộ của bạn vượt qua xác minh chữ ký, phiên bản đã deploy cũng vậy — trừ khi bạn chạm giới hạn bộ nhớ hoặc timeout phía serverless, khi đó triệu chứng khác (5xx, không phải 401).
Chi tiết Stripe xem hướng dẫn kiểm thử webhook Stripe cục bộ. Toán chữ ký nói chung, hướng dẫn xác minh chữ ký. Tham gia danh sách chờ PortPreview để có một tunnel thân thiện với Next.js kèm capture.