Skip to content

Keep the request body a plain Readable after middleware so Readable.toWeb() doesn't hang#95370

Open
UditDewan wants to merge 1 commit into
vercel:canaryfrom
UditDewan:fix/body-stream-readable-towebstream
Open

Keep the request body a plain Readable after middleware so Readable.toWeb() doesn't hang#95370
UditDewan wants to merge 1 commit into
vercel:canaryfrom
UditDewan:fix/body-stream-readable-towebstream

Conversation

@UditDewan

Copy link
Copy Markdown

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

`replaceRequestBody` grafts the buffered body stream onto the original
IncomingMessage by copying its enumerable properties. The buffered stream
was a `PassThrough` (a Duplex), so its writable-side internals
(`_writableState` and the Writable methods) were copied onto the request.
Node stream utilities such as `Readable.toWeb()` then saw an unfinished
writable side and hung, so a route handler reading a POST body after
middleware ran (`NextResponse.next()`) never completed.

The buffered stream is only ever fed via `.push()`, so it never needs the
writable side. Making it a plain `Readable` keeps the request a pure
Readable and fixes the hang.

Fixes vercel#95335

Co-authored-by: Baradhan-Madhu <26barum@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant