Tất cả bài viết
Next.jsApp Routerwebhook debugginglocal testing

Webhook Next.js trên localhost, làm đúng cách

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

  1. Chạy next dev (hoặc npm run dev) — thường cổng 3000.
  2. Ở terminal khác, npx portpreview 3000.
  3. Dán URL HTTPS vào cấu hình webhook của nhà cung cấp. Với Stripe: Developers → Webhooks → Add endpoint.
  4. 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.

Câu hỏi thường gặp

Làm sao lấy body thô trong một webhook Next.js App Router?
Gọi await request.text() bên trong handler POST. Đừng gọi request.json() trước — một khi body đã được đọc, gọi .text() hoặc .json() lần thứ hai không trả gì.
Tôi có cần bodyParser: false trong Next.js App Router không?
Không. Config đó là mẫu Pages Router và không có tương đương trong App Router. Body được đọc theo nhu cầu từ đối tượng Request, và bạn đọc nó dưới dạng văn bản thô bất cứ khi nào cần xác minh chữ ký.
Tôi có thể chạy một route webhook trên edge runtime của Next.js không?
Có thể, nhưng các SDK xác minh chữ ký đôi khi dựa vào API crypto của Node không có trên edge. Hãy giữ Node runtime mặc định cho webhook trừ khi bạn có lý do cụ thể để chuyển.