mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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 <[email protected]> Co-authored-by: Baradhan-Madhu <[email protected]>
47 lines
1.1 KiB
TypeScript
47 lines
1.1 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('proxy request body with Readable.toWeb', () => {
|
|
const { next } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
|
|
it('allows a pages api route to read the proxied body with Readable.toWeb()', async () => {
|
|
const body = JSON.stringify({ hello: 'world' })
|
|
|
|
const res = await next.fetch('/api/echo', {
|
|
body,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
method: 'POST',
|
|
})
|
|
const data = await res.json()
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(data).toEqual({
|
|
echo: { hello: 'world' },
|
|
writableState: false,
|
|
write: 'undefined',
|
|
end: 'undefined',
|
|
})
|
|
})
|
|
|
|
it('still allows a pages api route to read the proxied body with node events', async () => {
|
|
const body = JSON.stringify({ hello: 'world' })
|
|
|
|
const res = await next.fetch('/api/echo-events', {
|
|
body,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
method: 'POST',
|
|
})
|
|
const data = await res.json()
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(data).toEqual({
|
|
echo: { hello: 'world' },
|
|
})
|
|
})
|
|
})
|