Найбільше джерело зламаних обробників вебхуків у 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
- Запустіть
next dev(абоnpm run dev) — зазвичай порт 3000. - В іншому терміналі
npx portpreview 3000. - Вставте HTTPS-URL у конфігурацію вебхука провайдера. Для Stripe: Developers → Webhooks → Add endpoint.
- Запустіть тестову подію. Запит влучає у ваш 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 тунель із захопленням.