NextJS copies `_writableState` to a Readable object, breaks Node `Readable.toWeb()` on POST routes
Middlewareinvalid link
### 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
```bash
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()`.
关闭于 2026-07-01 1 条评论