ITADN

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

#95334Closedabir-taheer 创建于 2026-07-01
Middlewareinvalid link
A
abir-taheercommented
### 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 条评论