Wszystkie artykuły
Next.jsApp Routerwebhook debugginglocal testing

Webhooki Next.js na localhost, zrobione dobrze

Zdecydowanie największym źródłem zepsutych handlerów webhooków w Next.js App Router jest surowe ciało. App Router jest w porządku. Route handlery są w porządku. Ale konwencje pobierania nieparsowanego ciała żądania zmieniły się między Pages Router a App Router, a większość integracji Stripe / GitHub / Shopify kopiuje fragmenty ze starego świata. Ten artykuł to nowy świat.

Reguła dwóch linii

W route handlerze App Routera pobierz surowe ciało przez await request.text(). Nie używaj request.json() przed weryfikacją podpisu. Nie próbuj wyłączać bodyParser tak jak w Pages Router — ta flaga już nie istnieje.

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

Trzy linie zasługują na uwagę: request.text() daje surowy string, który podpisał Stripe. headers() w App Router jest async — zauważ await. A weryfikacja podpisu musi nastąpić przed jakimkolwiek odpowiednikiem JSON.parse.

Nawyk Pages Router do oduczenia

Jeśli migrowałeś z Pages Router, pamiętasz pisanie tego:

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

Tej konfiguracji nie ma w App Router. Ciało jest czytane na żądanie z obiektu Request. Cokolwiek czytasz jako drugie, jest puste — request.text() po request.json() nie zwraca nic. Wybierz jedno, a dla webhooków zawsze wybierz text().

Konfiguracja tunelu dla dev Next.js

  1. Uruchom next dev (lub npm run dev) — zwykle port 3000.
  2. W innym terminalu npx portpreview 3000.
  3. Wklej URL HTTPS do konfiguracji webhooka swojego dostawcy. Dla Stripe: Developers → Webhooks → Add endpoint.
  4. Wyzwól zdarzenie testowe. Żądanie trafia do twojego route handlera z wszystkimi nagłówkami nietkniętymi.

Jeśli używasz next dev --turbo, to nadal działa. Turbopack zmienia bundler, nie model request/response.

Edge runtime: nie

Jeśli ustawiłeś export const runtime = 'edge' na route handlerze webhooka, stracisz dostęp do API crypto tylko-Node, których używają niektóre SDK dostawców. Stripe Node SDK działa na edge w niedawnych wersjach, ale weryfikacja podpisu z dowolnymi dostawcami jest niepewna. Trzymaj trasy webhooków na runtime Node, chyba że masz konkretny powód.

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

Pułapki App Routera, na które wciąż wpadamy

Zwrócenie złego typu

Jeśli zwracasz new Response(JSON.stringify(...)) zamiast NextResponse.json(...), nagłówek content-type może nie zostać ustawiony. Niektórzy dostawcy się tym przejmują. Używaj NextResponse.json() lub ustaw nagłówek ręcznie.

Streaming i websockety

Handlery webhooków to krótkotrwałe żądania HTTP, ale jeśli tunelujesz aplikację Next.js, która ma też trasy WebSocket (na przykład z socket.io na własnym serwerze), upewnij się, że twój tunel zachowuje upgrade'y WebSocket. Większość zachowuje; sprawdź dokumentację.

Middleware czytające ciało

Jeśli masz middleware.ts w katalogu głównym, który wywołuje request.text() do logowania lub auth, twoja trasa webhooka nigdy nie widzi ciała. Pomiń ścieżkę webhooka w matcherze lub czytaj ciała tylko w trasach, które ich potrzebują.

Deploye Vercel cię nie uratują

Bugi webhooków zależne od parsowania surowego ciała zachowują się identycznie na Vercel i na localhost. Gdy twój lokalny test w tunelu przejdzie weryfikację podpisu, wdrożona wersja też przejdzie — chyba że trafiasz na limit pamięci lub timeoutu po stronie serverless, w którym to przypadku objaw jest inny (5xx, nie 401).

Po szczegóły Stripe zobacz przewodnik lokalnego testowania webhooków Stripe. Po matematykę podpisu ogólnie — przewodnik weryfikacji podpisu. Dołącz do listy oczekujących PortPreview, by dostać przyjazny dla Next.js tunel z przechwytywaniem.

Najczęściej zadawane pytania

Jak pobrać surowe ciało w webhooku Next.js App Router?
Wywołaj await request.text() wewnątrz handlera POST. Nie wywołuj najpierw request.json() — gdy ciało zostanie odczytane, drugie wywołanie .text() lub .json() nic nie zwraca.
Czy potrzebuję bodyParser: false w Next.js App Router?
Nie. Ta konfiguracja była wzorcem Pages Router i nie ma odpowiednika w App Router. Ciało jest czytane na żądanie z obiektu Request, a czytasz je jako surowy tekst, gdy musisz zweryfikować podpis.
Czy mogę uruchomić trasę webhooka na edge runtime Next.js?
Możesz, ale SDK weryfikacji podpisu czasem polegają na API crypto Node niedostępnych na edge. Zostań przy domyślnym runtime Node dla webhooków, chyba że masz konkretny powód do zmiany.