Alle Artikel
Next.jsApp Routerwebhook debugginglocal testing

Next.js-Webhooks auf localhost, richtig gemacht

Die mit Abstand größte Quelle kaputter Webhook-Handler im Next.js App Router ist der Raw Body. Der App Router ist in Ordnung. Route Handler sind in Ordnung. Aber die Konventionen zum Erfassen des ungeparsten Request-Bodys haben sich zwischen Pages Router und App Router geändert, und die meisten Stripe-/GitHub-/Shopify-Integrationen kopieren Snippets aus der alten Welt. Dieser Artikel ist die neue Welt.

Die Zwei-Zeilen-Regel

In einem App-Router-Route-Handler holst du den Raw Body mit await request.text(). Nutze nicht request.json() vor der Signaturprüfung. Versuche nicht, bodyParser so wie im Pages Router zu deaktivieren — dieses Flag existiert nicht mehr.

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

Drei Zeilen verdienen Aufmerksamkeit: request.text() gibt dir den rohen String, den Stripe signiert hat. headers() im App Router ist async — beachte das await. Und die Signaturprüfung muss vor jedem JSON.parse-Äquivalent passieren.

Die Pages-Router-Gewohnheit, die du verlernen musst

Wenn du vom Pages Router migriert bist, erinnerst du dich, das hier geschrieben zu haben:

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

Diese Config existiert im App Router nicht. Der Body wird bei Bedarf aus dem Request-Objekt gelesen. Was du als Zweites liest, ist leer — request.text() nach request.json() gibt nichts zurück. Wähle eines, und für Webhooks immer text().

Tunnel-Setup für Next.js-Dev

  1. Führe next dev (oder npm run dev) aus — meist Port 3000.
  2. In einem anderen Terminal npx portpreview 3000.
  3. Füge die HTTPS-URL in die Webhook-Konfiguration deines Anbieters ein. Für Stripe: Developers → Webhooks → Add endpoint.
  4. Löse ein Testevent aus. Der Request trifft deinen Route Handler mit allen Headern intakt.

Wenn du next dev --turbo nutzt, funktioniert das weiterhin. Turbopack ändert den Bundler, nicht das Request/Response-Modell.

Edge Runtime: lieber nicht

Wenn du export const runtime = 'edge' auf einem Webhook-Route-Handler gesetzt hast, verlierst du Zugriff auf Node-only-Crypto-APIs, die manche Anbieter-SDKs nutzen. Das Stripe-Node-SDK funktioniert in neueren Versionen auf Edge, aber Signaturprüfung mit beliebigen Anbietern ist mal so, mal so. Halte Webhook-Routen auf der Node-Runtime, außer du hast einen bestimmten Grund.

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

App-Router-Stolperfallen, in die wir immer wieder tappen

Den falschen Typ zurückgeben

Wenn du new Response(JSON.stringify(...)) statt NextResponse.json(...) zurückgibst, wird der content-type-Header womöglich nicht gesetzt. Manchen Anbietern ist das wichtig. Nutze NextResponse.json() oder setze den Header manuell.

Streaming und WebSockets

Webhook-Handler sind kurzlebige HTTP-Requests, aber wenn du eine Next.js-App tunnelst, die auch WebSocket-Routen hat (zum Beispiel mit socket.io auf einem Custom Server), stelle sicher, dass dein Tunnel WebSocket-Upgrades erhält. Die meisten tun das; prüfe die Docs.

Middleware, die den Body liest

Wenn du eine middleware.ts im Root hast, die request.text() fürs Logging oder Auth aufruft, sieht deine Webhook-Route den Body nie. Überspringe den Webhook-Pfad in deinem Matcher oder lies Bodys nur in Routen, die sie brauchen.

Vercel-Deploys retten dich nicht

Webhook-Bugs, die vom Raw-Body-Parsing abhängen, verhalten sich auf Vercel und localhost identisch. Sobald dein lokaler Tunnel-Test die Signaturprüfung besteht, tut es die deployte Version auch — außer du stößt auf ein Memory- oder Timeout-Limit auf der Serverless-Seite, dann ist das Symptom anders (5xx, nicht 401).

Für Stripe-Spezifika siehe den Leitfaden zum lokalen Stripe-Webhook-Testing. Für die Signatur-Mathematik allgemein den Signaturprüfungs-Leitfaden. Tritt der PortPreview-Warteliste bei, um einen Next.js-freundlichen Tunnel mit Capture zu bekommen.

Häufig gestellte Fragen

Wie bekomme ich den Raw Body in einem Next.js-App-Router-Webhook?
Rufe await request.text() im POST-Handler auf. Rufe nicht zuerst request.json() auf — sobald der Body gelesen wurde, gibt ein zweiter Aufruf von .text() oder .json() nichts zurück.
Brauche ich bodyParser: false im Next.js App Router?
Nein. Diese Config war ein Pages-Router-Pattern und hat im App Router kein Äquivalent. Der Body wird bei Bedarf aus dem Request-Objekt gelesen, und du liest ihn als Raw Text, wann immer du eine Signatur prüfen musst.
Kann ich eine Webhook-Route auf der Next.js-Edge-Runtime laufen lassen?
Du kannst, aber Signaturprüfungs-SDKs verlassen sich manchmal auf Node-Crypto-APIs, die auf Edge nicht verfügbar sind. Bleib bei der Standard-Node-Runtime für Webhooks, außer du hast einen bestimmten Grund zu wechseln.