Skip to content

Commit 7774ea3

Browse files
committed
Try to replace the underlying stream of IncomingMessage after we read it
1 parent d77d43b commit 7774ea3

File tree

3 files changed

+104
-16
lines changed

3 files changed

+104
-16
lines changed

packages/next/server/base-http/node.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { NEXT_REQUEST_META, RequestMeta } from '../request-meta'
77

88
import { BaseNextRequest, BaseNextResponse } from './index'
99

10+
type Req = IncomingMessage & {
11+
[NEXT_REQUEST_META]?: RequestMeta
12+
cookies?: NextApiRequestCookies
13+
}
14+
1015
export class NodeNextRequest extends BaseNextRequest<Readable> {
1116
public headers = this._req.headers;
1217

@@ -21,12 +26,11 @@ export class NodeNextRequest extends BaseNextRequest<Readable> {
2126
return this._req
2227
}
2328

24-
constructor(
25-
private _req: IncomingMessage & {
26-
[NEXT_REQUEST_META]?: RequestMeta
27-
cookies?: NextApiRequestCookies
28-
}
29-
) {
29+
set originalRequest(value: Req) {
30+
this._req = value
31+
}
32+
33+
constructor(private _req: Req) {
3034
super(_req.method!.toUpperCase(), _req.url!, _req)
3135
}
3236

packages/next/server/next-server.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url'
88
import type { PrerenderManifest } from '../build'
99
import type { Rewrite } from '../lib/load-custom-routes'
1010
import type { BaseNextRequest, BaseNextResponse } from './base-http'
11-
import type { ReadableStream as ReadableStreamPolyfill } from 'next/dist/compiled/web-streams-polyfill/ponyfill'
1211
import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill'
12+
import NodeStreams from 'stream'
1313

1414
import { execOnce } from '../shared/lib/utils'
1515
import {
@@ -40,7 +40,7 @@ import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
4040
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
4141
import { format as formatUrl, UrlWithParsedQuery } from 'url'
4242
import compression from 'next/dist/compiled/compression'
43-
import Proxy from 'next/dist/compiled/http-proxy'
43+
import HttpProxy from 'next/dist/compiled/http-proxy'
4444
import { route } from './router'
4545
import { run } from './web/sandbox'
4646

@@ -92,6 +92,8 @@ export interface NodeRequestHandler {
9292
): Promise<void>
9393
}
9494

95+
type BodyStream = ReadableStream<Uint8Array>
96+
9597
export default class NextNodeServer extends BaseServer {
9698
private imageResponseCache?: ResponseCache
9799

@@ -486,7 +488,7 @@ export default class NextNodeServer extends BaseServer {
486488
parsedUrl.search = stringifyQuery(req, query)
487489

488490
const target = formatUrl(parsedUrl)
489-
const proxy = new Proxy({
491+
const proxy = new HttpProxy({
490492
target,
491493
changeOrigin: true,
492494
ignorePath: true,
@@ -1306,6 +1308,14 @@ export default class NextNodeServer extends BaseServer {
13061308
}
13071309
}
13081310

1311+
if (originalBody) {
1312+
const noderequest = params.request as NodeNextRequest
1313+
noderequest.originalRequest = enhanceIncomingMessage(
1314+
noderequest.originalRequest,
1315+
originalBody.original()
1316+
)
1317+
}
1318+
13091319
return result
13101320
}
13111321

@@ -1346,9 +1356,7 @@ export default class NextNodeServer extends BaseServer {
13461356
/**
13471357
* Creates a ReadableStream from a Node.js HTTP request
13481358
*/
1349-
function requestToBodyStream(
1350-
request: IncomingMessage
1351-
): ReadableStreamPolyfill<Uint8Array> {
1359+
function requestToBodyStream(request: IncomingMessage): BodyStream {
13521360
const transform = new TransformStream<Uint8Array, Uint8Array>({
13531361
start(controller) {
13541362
request.on('data', (chunk) => controller.enqueue(chunk))
@@ -1357,21 +1365,61 @@ function requestToBodyStream(
13571365
},
13581366
})
13591367

1360-
return transform.readable
1368+
return transform.readable as unknown as ReadableStream<Uint8Array>
13611369
}
13621370

13631371
/**
13641372
* A simple utility to take an original stream and have
13651373
* an API to duplicate it without closing it or mutate any variables
13661374
*/
1367-
function teeableStream<T>(originalStream: ReadableStreamPolyfill<T>): {
1368-
duplicate(): ReadableStreamPolyfill<T>
1375+
function teeableStream<T>(originalStream: ReadableStream<T>): {
1376+
duplicate(): ReadableStream<T>
1377+
original(): ReadableStream<T>
13691378
} {
13701379
return {
13711380
duplicate() {
13721381
const [stream1, stream2] = originalStream.tee()
13731382
originalStream = stream1
13741383
return stream2
13751384
},
1385+
original() {
1386+
return originalStream
1387+
},
13761388
}
13771389
}
1390+
1391+
function bodyStreamToNodeStream(bodyStream: BodyStream): NodeStreams.Readable {
1392+
const reader = bodyStream.getReader()
1393+
return NodeStreams.Readable.from(
1394+
(async function* () {
1395+
while (true) {
1396+
const { done, value } = await reader.read()
1397+
if (done) {
1398+
return
1399+
}
1400+
yield value
1401+
}
1402+
})()
1403+
)
1404+
}
1405+
1406+
function enhanceIncomingMessage<T extends IncomingMessage>(
1407+
base: T,
1408+
body: BodyStream
1409+
): T {
1410+
const stream = bodyStreamToNodeStream(body)
1411+
return new Proxy<T>(base, {
1412+
get(target, name) {
1413+
if (name in stream) {
1414+
const v = stream[name]
1415+
if (typeof v === 'function') {
1416+
return v.bind(stream)
1417+
} else {
1418+
return v
1419+
}
1420+
}
1421+
1422+
return target[name]
1423+
},
1424+
})
1425+
}

test/production/reading-request-body-in-middleware/index.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ describe('reading request body in middleware', () => {
1919
const json = await request.json();
2020
2121
if (request.nextUrl.searchParams.has("next")) {
22-
return NextResponse.next();
22+
const res = NextResponse.next();
23+
res.headers.set('x-from-root-middleware', '1');
24+
return res;
2325
}
2426
2527
return new Response(JSON.stringify({
@@ -55,6 +57,15 @@ describe('reading request body in middleware', () => {
5557
})
5658
}
5759
`,
60+
61+
'pages/api/hi.js': `
62+
export default function hi(req, res) {
63+
res.json({
64+
...req.body,
65+
api: true,
66+
})
67+
}
68+
`,
5869
},
5970
dependencies: {},
6071
})
@@ -105,4 +116,29 @@ describe('reading request body in middleware', () => {
105116
root: false,
106117
})
107118
})
119+
120+
it('passes the body to the api endpoint', async () => {
121+
const response = await fetchViaHTTP(
122+
next.url,
123+
'/api/hi',
124+
{
125+
next: '1',
126+
},
127+
{
128+
method: 'POST',
129+
headers: {
130+
'content-type': 'application/json',
131+
},
132+
body: JSON.stringify({
133+
foo: 'bar',
134+
}),
135+
}
136+
)
137+
expect(response.status).toEqual(200)
138+
expect(await response.json()).toEqual({
139+
foo: 'bar',
140+
api: true,
141+
})
142+
expect(response.headers.get('x-from-root-middleware')).toEqual('1')
143+
})
108144
})

0 commit comments

Comments
 (0)