La principale cause de webhooks cassés dans Next.js App Router : le corps brut. L'App Router fonctionne. Les route handlers fonctionnent. Mais les conventions pour récupérer le corps non parsé ont changé entre Pages Router et App Router, et la plupart des intégrations Stripe / GitHub / Shopify copient des snippets de l'ancien monde.
La règle en deux lignes
Dans un route handler App Router, récupérez le corps brut avec await request.text(). N'appelez pas request.json() avant la vérification de signature. Ne tentez pas bodyParser: false — ce flag n'existe plus.
// 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();
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 },
);
}
return NextResponse.json({ received: true });
}
request.text() donne la chaîne brute signée par Stripe. headers() est async dans App Router. La vérification doit précéder tout équivalent JSON.parse.
L'habitude Pages Router à oublier
// pages/api/webhooks/stripe.ts (ANCIEN)
export const config = { api: { bodyParser: false } };
Cette config n'existe plus en App Router. Le corps est lu à la demande. La deuxième lecture renvoie rien — request.text() après request.json() renvoie vide.
Setup tunnel pour Next.js
- Lancez
next dev(port 3000 généralement). - Dans un autre terminal,
npx portpreview 3000. - Collez l'URL HTTPS dans la config webhook du provider.
- Déclenchez un événement. La requête arrive avec en-têtes intacts.
Compatible avec next dev --turbo.
Edge runtime : à éviter
Si vous avez export const runtime = 'edge' sur une route webhook, certains SDKs perdent accès à des APIs crypto Node. Le SDK Stripe fonctionne en edge en versions récentes mais la vérification de signature avec des providers arbitraires est aléatoire.
export const runtime = 'nodejs';
Pièges App Router récurrents
Mauvais type de retour
Si vous retournez new Response(JSON.stringify(...)) au lieu de NextResponse.json(...), le content-type peut manquer. Certains providers s'en plaignent.
Streaming et WebSockets
Les handlers webhook sont des requêtes HTTP courtes. Si vous tunnelisez aussi des routes WebSocket, vérifiez que le tunnel préserve l'upgrade.
Middleware qui lit le corps
Un middleware.ts qui appelle request.text() pour logger ou auth-ifier vide le corps avant la route webhook. Excluez le chemin webhook du matcher.
Vercel ne vous sauvera pas
Les bugs de corps brut se comportent à l'identique sur Vercel et localhost. Si le test local en tunnel passe, le déploiement aussi — sauf limites mémoire/timeout serverless (symptômes 5xx, pas 401).
Voir test webhook Stripe en local et guide vérification de signature. Rejoignez la waitlist PortPreview.