Next.js Edge Middleware Blocked Our Client Webhook at 2 AM
A silent 401 from Clerk middleware killed our Stripe webhook handler in production. Here is the exact fix we shipped.
Author
The 2 AM Pager: When Stripe Webhooks Vanish Into Next.js Middleware
At 2:17 AM IST, our phone buzzed. A client on Next.js 14 App Router, hosted on Vercel, had stopped receiving Stripe webhook events. The dashboard stayed empty. Stripe kept retrying every 3 minutes. Our logs showed nothing. No errors. No traces. Just a silent 401 that never reached our handler.
We spent the next hour chasing ghosts. The route file looked correct. The secret was right. The signature check was in place. But Stripe saw 401s and we saw zero logs.
This is the story of how Next.js Edge Middleware, Clerk auth, and a deprecated config option combined to eat our webhook alive.
What We Tried First (And Why It Failed)
We assumed the classic Pages Router pattern still worked in App Router. It did not.
First mistake: we kept export const config = { api: { bodyParser: false } } in our route file. In App Router, this config is silently ignored. It does nothing. Stripe sends a raw body. Our handler called request.json() before signature verification. The stream was consumed. The signature check failed. Stripe saw 401s.
Second mistake: we never excluded the webhook path from Clerk middleware. Clerk runs on every route by default in production. Stripe is not an authenticated user. The middleware intercepted our webhook route and returned 401 before our handler executed. No log entry. No trace.
Third mistake: we used Buffer.from() for HMAC verification. The route runs on Vercel Edge Runtime. Buffer does not exist there. The handler threw a runtime error. Stripe saw 401s. We saw nothing.
All three silently broke the handler. Stripe retried. Our logs stayed empty.
The Working Fix: Three Real Changes That Stuck
1. Removed Deprecated Config, Switched to request.text()
We deleted export const config = { api: { bodyParser: false } } from /app/api/webhooks/stripe/route.ts. In App Router, body parsing is handled by the Web Fetch API. We switched to await request.text() before any body read, then JSON.parse() after signature verification.
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
});
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get('stripe-signature');
if (!sig) {
return NextResponse.json({ error: 'No signature' }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
// handle your session here
}
return NextResponse.json({ received: true });
}
2. Excluded /api/webhooks from Clerk Middleware
We opened /src/middleware.ts and added the webhook path to the exclusion matcher. Clerk middleware now skips /api/webhooks entirely.
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/account(.*)']);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) {
auth().protect();
}
});
export const config = {
matcher: [
'/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)',
'/(api|trpc)(.*)',
],
};
3. Replaced Buffer.from() with TextEncoder and crypto.subtle
The route runs on Vercel Edge Runtime. Buffer does not exist. We replaced Buffer.from() with TextEncoder and crypto.subtle for HMAC verification.
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(process.env.STRIPE_WEBHOOK_SECRET!),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const expectedSignature = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
const expectedHex = Array.from(new Uint8Array(expectedSignature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
Commands and File Paths We Actually Used
- Route file:
/app/api/webhooks/stripe/route.ts - Middleware exclusion in
/src/middleware.ts:
export const config = {
matcher: [
'/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)',
'/(api|trpc)(.*)',
],
};
- Signature verification using
TextEncoderandcrypto.subtle.importKeyinstead ofBuffer.from
Pitfalls We Would Warn an Intern About
- Never call
request.json()before verifying the webhook signature. The stream is consumed and you cannot rewind it. export const config = { api: { bodyParser: false } }is silently ignored in App Router. It does nothing. Remove it.- Auth middleware (Clerk, NextAuth, Better Auth) returns 401 before your handler runs. No log entry. Always exclude webhook paths.
Bufferdoes not exist on Edge Runtime. UseTextEncoderand the Web Crypto API.- Comparing
req.method === 'post'instead of'POST'causes timeouts. HTTP methods are always uppercase.
What We Would Do Differently Next Time
We would route all webhooks through HookRelay as a buffer layer. Instead of Stripe hitting our API route directly, HookRelay would receive the event, handle retries and signature verification, and forward only verified events to our Next.js handler. This decouples delivery reliability from our application code and gives us a replayable event log when things go wrong.
We would also add a vercel.json with explicit route rewrites to avoid any ambiguity in how Vercel routes external callbacks.
The next time Stripe delivers a webhook at 2 AM, we want it to land in our handler, not in middleware.
Sources
- Fix Stripe Webhook in Next.js App Router, HttpFixer
- WebhookFix · HttpFixer
- Next.js 14 Webhook Changes | Migration Guide | HookRelay
- Stripe Webhook in Nextjs issue #48885
- Next.js Middleware Ate My POST Body (2026 Fix) | HeyDev
- Middleware Not Triggered for Safaricom Callback in Next.js Hosted on Vercel - Vercel Community
- Stripe Webhook Hell: Every Gotcha (and How to Avoid It)
- Next.js App Router + Clerk Auth: A Practical Setup Guide | RAXXO Studios
Sources
Related reading
Enjoyed this article?
Back to Blog


