Next.js Middleware Ate My POST Body (2026 Fix)

Yatish Goel

Yatish Goel

Co-Founder & CTO

February 22, 2026
#Next.js#middleware#Netlify#Vercel#App Router#webhooks#2026

You ship a harmless middleware. Maybe a tiny auth gate, maybe just a header. Then your POST requests start showing up with an empty body. Or worse, you get a runtime error like: body is unusable, body already consumed, stream already read. It only happens in prod. Local is fine. That is the most annoying kind of bug.

We have seen this a few times in 2025 and 2026, mostly on US-based startups shipping Next.js App Router on Netlify or Vercel. The pattern is always the same: a checkout, a webhook, or a form submit hits an API route or route handler, middleware runs first, and suddenly the body is gone when your handler tries to read it.

This post is the fix we wish existed the first time we hit it. Not a theory dump. What to check, what to change, and what it costs if you ignore it.

The exact failure modes we see in 2026

There are three common versions of this bug:

1) Empty body: request.json() returns {} or throws because the stream is empty. Your logs show Content-Length, but the handler gets nothing.

2) Double-read: your middleware or some helper reads the body (or triggers a read) and your route handler reads it again. The second read blows up.

3) Oversize buffering: middleware clones and buffers the body, hits a limit, and you get weird behavior. Sometimes it is a hard error. Sometimes it looks like the body vanished.

The Next.js docs spell out an important detail: when middleware is used, Next.js clones the request body and buffers it in memory so it can be read more than once. That is nice, but it also means there is memory involved and there is a size limit. If you are sending large payloads, you can hit it.

Why this happens (the simple mental model)

In App Router, your handler receives a Web Request, not the old Express-ish req. The body is a stream. Streams are single-use unless someone clones and buffers them.

Middleware sits in front. If your middleware code calls request.json(), request.text(), request.formData(), or hands request to something that does, it can consume the stream. If the platform or Next.js cannot safely clone it for your later handler, you are done.

Even if you do not call request.json() directly, you can still trigger a read. Classic example: logging helpers that stringify the request, validation libs that peek at the body, or a debugging branch you forgot to remove.

A quick checklist before you touch code

Do these checks first. They save hours.

1) Find every place you parse the body

Search for: request.json(), request.text(), request.formData(), and any helper that receives the request object.

If middleware reads the body, assume it is guilty until proven innocent.

2) Confirm it is not your client

If you use fetch() in the browser, confirm you are not doing something like reading the same response twice. That is a different bug. Here we are talking about the incoming request.

3) Check payload size

If this breaks on one specific route, log the rough size. Stripe webhooks, analytics events, and multipart uploads can be much bigger than you think.

The fix that works 90% of the time: stop reading the body in middleware

If you can avoid it, avoid it. Middleware should be dumb. Headers, cookies, URL, method, that is it.

Bad middleware patterns:

- Parse JSON in middleware to check a field.
- Run validation in middleware.
- Log request body in middleware.

Good middleware patterns:

- Check request.nextUrl.pathname.
- Check request.cookies.
- Check request.headers.get('authorization').
- Redirect or rewrite.
- Add response headers.

If you need to validate JSON for security, do it in the route handler where you own the read.

The boring, real-world fix: skip middleware for API and webhooks

Most teams do not need middleware on every path. They just enabled it globally because it was easy.

In 2026, a pragmatic rule is: do not run middleware for routes that must read the body reliably. That includes:

- /api/webhooks/* (Stripe, Clerk, Supabase, etc).
- /api/upload/*.
- Any POST that handles multipart/form-data.
- Payment callbacks.

Example matcher that skips API:

export const config = { matcher: ['/((?!api).*)'] }

Or be explicit and only match the pages you truly need, like /dashboard and /account.

If you must run middleware: do not parse

Sometimes you really want middleware on API routes. Like tenant routing or bot filtering. Fine. But do not parse the body.

If you need something from the body, move that route to a route handler that does auth itself. Middleware is not the right tool.

If you insist anyway, treat the body as single-use. Once you read it, you own the consequences.

The size limit trap: middleware body buffering

Next.js has a config option for middleware body buffering size: experimental.middlewareClientMaxBodySize.

The doc line that matters is: middleware clones the request body and buffers it in memory to enable multiple reads. There is a default limit, and it exists so your edge runtime does not get wrecked by huge bodies.

If you are seeing this bug only when payloads get chunky, you may be hitting that limit. Fix options:

- Make the payload smaller (best).
- Move that route off middleware (usually best).
- Raise the limit (only if you know what you are doing).

Raising the limit is a trade. Bigger buffering means more memory per request. At 500 concurrent requests, a 10MB buffer is already scary. Multiply it and you are in paging hell.

A concrete case study (anonymized, but real numbers)

We worked with a UK founder selling B2B training. Next.js App Router on Netlify. They added middleware for basic auth and geo-based redirects. After that, their contact form started failing in prod about 3-5% of the time.

Symptoms:
- POST /api/lead would sometimes throw body already consumed.
- Logs showed requests were coming in, but request.json() failed.
- Local dev never reproduced.

Root cause: a debug branch in middleware was doing await request.text() to log raw requests for a single IP range. They forgot to remove it. In prod, that IP range included their own office NAT, so it looked random.

Fix:
- Removed body logging from middleware.
- Narrowed matcher to only /dashboard and /account.
- Added server-side validation in the route handler.

Time and cost:
- 3.5 hours to trace and patch.
- About $900-$1,400 in engineering time depending on rates.
- The bigger cost was lost leads. Their ads were running at about $120/day. Dropping 3% of leads for two weeks was a quiet, dumb tax.

The route handler side: read once, then pass data around

Even after you fix middleware, you can still shoot yourself in the foot in the handler.

Rule: parse once. Store the result. Do not call request.json() twice.

Bad:
const body1 = await request.json();
const body2 = await request.json();

Good:
const body = await request.json();
validate(body);
await save(body);

If you need raw text for signature checks (Stripe, webhook signing), read text once, verify, then JSON.parse on that string.

Netlify vs Vercel: what changes?

The core issue is the same. The difference is how often you notice it.

On Vercel, middleware runs at the edge by default, which makes body handling feel stricter. On Netlify, you can still end up in edge-ish contexts depending on how it is deployed. Either way, assume the body is fragile in middleware.

If you are a founder in the US/UK and you are scaling paid traffic, treat this like a revenue bug. It is not a cute engineering problem.

What I would ship today (2026) on a fresh Next.js app

1) Middleware matcher only for pages that need it. Not global.

2) No body parsing in middleware. Ever.

3) Webhooks live under /api/webhooks/* and are excluded from middleware.

4) Route handlers parse once. Validation and auth live there.

5) If you need request logging, log headers and path in middleware, and log bodies only inside handlers, behind a feature flag.

Debugging tips when it only happens in prod

- Add a request id header in middleware, return it in the response, and log it in your handler. That tells you if middleware ran.

- Log request.method, pathname, content-type, and content-length. Do not log full bodies in production.

- If you suspect size limits, temporarily reject bodies over a threshold with a clear error. You would rather fail loudly than drop data silently.

When to pay someone to fix this

If this bug touches checkout, webhooks, lead forms, or onboarding, fix it same day. If your team is stuck, pay for help. In most cases we can find the culprit in 2-6 hours because it is almost always one of:

- a global matcher that should not be global.
- a body read in middleware, sometimes hidden in a helper.
- a double-read in the handler.
- a payload size problem that shows up at scale.

Bottom line

Middleware is not a pre-handler where you can do whatever you want. Treat it like a bouncer checking IDs. If you ask people to empty their pockets, do not act surprised when the next room is missing stuff.

Ship middleware that is boring. Your POST bodies will stop disappearing.

---

Related reading

Yatish Goel

Yatish Goel

Co-Founder & CTO

US Startup ExperienceIIT Kanpur

Full-stack architect with US startup experience and an IIT Kanpur degree. Yatish drives the technical vision at HeyDev, designing robust architectures and leading development across web, mobile, and AI projects.

Related Articles