Files
Tim Neutkens 3bb780e7d6 Keep the request body a plain Readable after middleware so Readable.toWeb() doesn't hang (#95607)
Recreation of https://github.com/vercel/next.js/pull/95370

### What's the problem?

A `POST` (or `PUT`/`PATCH`) request that passes through middleware
returning `NextResponse.next()` hangs indefinitely when the downstream
handler reads the body via Node's `Readable.toWeb()`. The request never
completes and eventually times out.

Reproduction: https://github.com/abir-taheer/next-js-readable-stream-bug

### Root cause

When middleware runs, `runMiddleware` clones the request body and later
calls `finalize()`, which grafts the buffered stream back onto the
original `IncomingMessage` via `replaceRequestBody()`
(`packages/next/src/server/body-streams.ts`). `replaceRequestBody`
copies the buffered stream's enumerable properties onto the request.

The buffered stream (`p2`) was a `PassThrough` — a `Duplex` — so its
writable-side internals (`_writableState` plus the enumerable `Writable`
methods like `write`/`end`) were copied onto the `IncomingMessage`.
Because that `_writableState.finished` is `false`, Node stream utilities
that inspect it — including `Readable.toWeb()`, which uses
`finished()`/end-of-stream detection — treat the request as a still-open
writable stream and wait forever.

`NextResponse.rewrite()` is unaffected (it builds a new internal request
and skips this path), and `GET`/`HEAD` requests are fine because there
is no body to clone.

### The fix

`p2` is only ever fed with `.push()`, so it never needs a writable side.
Making it a plain `Readable` instead of a `PassThrough` keeps the
finalized request a pure `Readable`, so `Readable.toWeb()` (and any
other duck-typing based on `_writableState`) behaves correctly. No
behavior change for the existing consumers, which only read the stream.

### Testing

Added `test/unit/body-streams.test.ts`, which drives the real clone →
`finalize()` flow and asserts the finalized request:
- is no longer writable (`_writableState` is `undefined`), and
- is fully consumable via `Readable.toWeb()` (this hangs before the
fix).

Fixes #95335

<!-- NEXT_JS_LLM_PR -->

Co-authored-by: UditDewan <udit.dewan21@gmail.com>
Co-authored-by: Baradhan-Madhu <26barum@gmail.com>
2026-07-10 15:55:29 +02:00
..