Skip to content

NextJS copies _writableState to a Readable object, breaks Node Readable.toWeb() on POST routes #95335

Description

@abir-taheer

Link to the code that reproduces this issue

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

To Reproduce

  1. git clone https://github.com/abir-taheer/next-js-readable-stream-bug
  2. npm install && npm run dev
  3. bash test.sh

Current vs. Expected behavior

Current: POST requests that pass through middleware returning NextResponse.next() hang indefinitely when the downstream handler reads the body via Readable.toWeb(). The request never completes and times out.

Expected: The request body should be fully readable by the downstream handler regardless of how it consumes the stream. Readable.toWeb() is the standard Node.js API for converting a Readable to a Web ReadableStream — it should work on the IncomingMessage after middleware runs.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:12 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T6050
  Available memory (MB): 65536
  Available CPU cores: 18
Binaries:
  Node: 24.17.0
  npm: 11.13.0
  Yarn: N/A
  pnpm: 10.34.4
Relevant Packages:
  next: 16.2.6
  eslint-config-next: N/A
  react: 19.2.7
  react-dom: 19.2.7
  typescript: 6.0.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Middleware

Which stage(s) are affected? (Select all that apply)

next dev (local), next start (local)

Additional context

The root cause is in replaceRequestBody() (body-streams.ts), not in middleware itself. After middleware calls NextResponse.next() on a POST route, runMiddleware clones the body into a PassThrough (Duplex) and copies its properties back onto the IncomingMessage (Readable) via for...in. This copies _writableState and 39 Writable prototype methods onto the Readable. Node's Readable.toWeb() then sees _writableState.finished === false and hangs waiting for the writable side to close.

The timing is deterministic, _writableState.finished becomes true via process.nextTick(), but replaceRequestBody() runs in a microtask (from await endPromise in finalize()). Microtasks always run before nextTick, so it always copies finished === false regardless of body size.

NextResponse.rewrite() is unaffected because it creates a new internal request and doesn't go through replaceRequestBody(). GET requests are also fine since there's no body to clone. The issue only affects POST (or PUT/PATCH) through NextResponse.next().

Metadata

Metadata

Assignees

No one assigned

    Labels

    MiddlewareRelated to Next.js Middleware.

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions