Усі статті
Next.jsApp Routerwebhook debugginglocal testing

Вебхуки Next.js на localhost — правильно

Найбільше джерело зламаних обробників вебхуків у Next.js App Router — це сире тіло. App Router у порядку. Route handler у порядку. Але угоди щодо отримання нерозібраного тіла запиту змінилися між Pages Router та App Router, а більшість інтеграцій Stripe / GitHub / Shopify копіюють сніпети зі старого світу. Ця стаття — про новий світ.

Правило двох рядків

У route handler App Router отримуйте сире тіло через await request.text(). Не використовуйте request.json() до перевірки підпису. Не намагайтеся вимкнути bodyParser, як у Pages Router — цього прапорця більше немає.

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

Три рядки заслуговують уваги: request.text() дає сирий рядок, який підписав Stripe. headers() в App Router асинхронна — зверніть увагу на await. І перевірка підпису має відбутися до будь-якого еквівалента JSON.parse.

Звичка Pages Router, від якої треба відучитися

Якщо ви мігрували з Pages Router, ви пам’ятаєте, як писали це:

// pages/api/webhooks/stripe.ts (OLD)
export const config = { api: { bodyParser: false } };

Цієї config немає в App Router. Тіло читається на вимогу з об’єкта Request. Усе, що читаєте другим, порожнє — request.text() після request.json() нічого не повертає. Виберіть одне, а для вебхуків завжди вибирайте text().

Налаштування тунелю для dev Next.js

  1. Запустіть next dev (або npm run dev) — зазвичай порт 3000.
  2. В іншому терміналі npx portpreview 3000.
  3. Вставте HTTPS-URL у конфігурацію вебхука провайдера. Для Stripe: Developers → Webhooks → Add endpoint.
  4. Запустіть тестову подію. Запит влучає у ваш route handler з усіма заголовками недоторканими.

Якщо використовуєте next dev --turbo, це все одно працює. Turbopack змінює бандлер, а не модель request/response.

Edge runtime: краще ні

Якщо ви поставили export const runtime = 'edge' на route handler вебхука, ви втратите доступ до Node-only crypto-API, які використовують деякі SDK провайдерів. Stripe Node SDK працює на edge у недавніх версіях, але перевірка підпису з довільними провайдерами — як пощастить. Тримайте маршрути вебхуків на Node runtime, якщо немає конкретної причини.

export const runtime = 'nodejs'; // explicit, safe default for webhooks

Пастки App Router, на які ми постійно натрапляємо

Повернення неправильного типу

Якщо повертаєте new Response(JSON.stringify(...)) замість NextResponse.json(...), заголовок content-type може не встановитися. Деяким провайдерам це важливо. Використовуйте NextResponse.json() або встановлюйте заголовок вручну.

Стрімінг і вебсокети

Обробники вебхуків — короткоживучі HTTP-запити, але якщо ви тунелюєте застосунок Next.js, який також має WebSocket-маршрути (наприклад, з socket.io на кастомному сервері), переконайтеся, що тунель зберігає апгрейди WebSocket. Більшість зберігає; перевірте документацію.

Middleware, що читає тіло

Якщо у вас є middleware.ts у корені, що викликає request.text() для логування чи авторизації, ваш маршрут вебхука ніколи не бачить тіло. Пропустіть шлях вебхука в matcher або читайте тіла лише в маршрутах, яким вони потрібні.

Деплої на Vercel вас не врятують

Баги вебхуків, що залежать від розбору сирого тіла, поводяться однаково на Vercel і на localhost. Щойно ваш локальний тест через тунель проходить перевірку підпису, розгорнута версія теж пройде — хіба що ви впираєтеся в ліміт пам’яті або таймаута на serverless-стороні, тоді симптом інший (5xx, не 401).

Щодо специфіки Stripe див. посібник з локального тестування вебхуків Stripe. Щодо математики підпису загалом — посібник з перевірки підпису. Приєднайтеся до списку очікування PortPreview, щоб отримати дружній до Next.js тунель із захопленням.

Поширені запитання

Як отримати сире тіло у вебхуку Next.js App Router?
Викличте await request.text() всередині обробника POST. Не викликайте request.json() першим — щойно тіло прочитане, повторний виклик .text() або .json() нічого не повертає.
Чи потрібен bodyParser: false у Next.js App Router?
Ні. Ця config була патерном Pages Router і не має еквівалента в App Router. Тіло читається на вимогу з об’єкта Request, і ви читаєте його як сирий текст щоразу, коли треба перевірити підпис.
Чи можна запустити маршрут вебхука на edge runtime Next.js?
Можна, але SDK перевірки підпису іноді покладаються на Node crypto-API, недоступні на edge. Залишайтеся на стандартному Node runtime для вебхуків, якщо немає конкретної причини перемикатися.