A maior fonte de handlers de webhook quebrados no Next.js App Router é o corpo cru. O App Router está ok. Os route handlers estão ok. Mas as convenções para pegar o corpo não parseado mudaram entre Pages Router e App Router, e a maioria das integrações de Stripe / GitHub / Shopify copia trechos do mundo antigo. Este artigo é o mundo novo.
A regra de duas linhas
Num route handler do App Router, pegue o corpo cru com await request.text(). Não use request.json() antes da verificação de assinatura. Não tente desativar o bodyParser como fazia no Pages Router — esse flag não existe mais.
// 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 });
}
Três linhas merecem atenção: request.text() te dá a string crua que o Stripe assinou. headers() no App Router é async — note o await. E a verificação de assinatura tem que acontecer antes de qualquer equivalente a JSON.parse.
O hábito de Pages Router para desaprender
Se você migrou do Pages Router, lembra de escrever isto:
// pages/api/webhooks/stripe.ts (OLD)
export const config = { api: { bodyParser: false } };
Essa config não existe no App Router. O corpo é lido sob demanda do objeto Request. O que você ler em segundo lugar está vazio — request.text() depois de request.json() não retorna nada. Escolha um, e para webhooks sempre escolha text().
Configuração do túnel para o dev do Next.js
- Rode
next dev(ounpm run dev) — geralmente porta 3000. - Em outro terminal,
npx portpreview 3000. - Cole a URL HTTPS na configuração de webhook do seu provedor. Para Stripe: Developers → Webhooks → Add endpoint.
- Dispare um evento de teste. A requisição chega ao seu route handler com todos os headers intactos.
Se você usa next dev --turbo, isso ainda funciona. O Turbopack muda o bundler, não o modelo request/response.
Edge runtime: não
Se você definiu export const runtime = 'edge' num route handler de webhook, vai perder acesso a APIs de crypto só-Node que alguns SDKs de provedores usam. O SDK Node do Stripe funciona no edge em versões recentes, mas a verificação de assinatura com provedores arbitrários é incerta. Mantenha as rotas de webhook no runtime Node a menos que tenha um motivo específico.
export const runtime = 'nodejs'; // explicit, safe default for webhooks
Armadilhas do App Router que continuamos batendo
Retornar o tipo errado
Se você retorna new Response(JSON.stringify(...)) em vez de NextResponse.json(...), o header content-type pode não ser definido. Alguns provedores se importam. Use NextResponse.json() ou defina o header manualmente.
Streaming e websockets
Handlers de webhook são requisições HTTP de vida curta, mas se você tuneliza um app Next.js que também tem rotas WebSocket (por exemplo com socket.io num servidor custom), garanta que seu túnel preserve os upgrades de WebSocket. A maioria preserva; cheque a documentação.
Middleware lendo o corpo
Se você tem um middleware.ts na raiz que chama request.text() para logging ou auth, sua rota de webhook nunca vê o corpo. Pule o caminho do webhook no seu matcher ou leia corpos só nas rotas que precisam.
Os deploys na Vercel não vão te salvar
Bugs de webhook que dependem do parse do corpo cru se comportam de forma idêntica na Vercel e no localhost. Uma vez que seu teste local em túnel passa na verificação de assinatura, a versão deployada também passará — a menos que você esteja batendo num limite de memória ou timeout do lado serverless, caso em que o sintoma é diferente (5xx, não 401).
Para detalhes do Stripe veja o guia de teste local de webhooks do Stripe. Para a matemática da assinatura em geral, o guia de verificação de assinatura. Entre na lista de espera do PortPreview para obter um túnel amigável ao Next.js com captura.