Skip to content

Commit 90d22d3

Browse files
committed
Try to replace the underlying stream of IncomingMessage after we read it
1 parent 04177b8 commit 90d22d3

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

@@ -490,7 +492,7 @@ export default class NextNodeServer extends BaseServer {
490492
parsedUrl.search = stringifyQuery(req, query)
491493

492494
const target = formatUrl(parsedUrl)
493-
const proxy = new Proxy({
495+
const proxy = new HttpProxy({
494496
target,
495497
changeOrigin: true,
496498
ignorePath: true,
@@ -1310,6 +1312,14 @@ export default class NextNodeServer extends BaseServer {
13101312
}
13111313
}
13121314

1315+
if (originalBody) {
1316+
const noderequest = params.request as NodeNextRequest
1317+
noderequest.originalRequest = enhanceIncomingMessage(
1318+
noderequest.originalRequest,
1319+
originalBody.original()
1320+
)
1321+
}
1322+
13131323
return result
13141324
}
13151325

@@ -1350,9 +1360,7 @@ export default class NextNodeServer extends BaseServer {
13501360
/**
13511361
* Creates a ReadableStream from a Node.js HTTP request
13521362
*/
1353-
function requestToBodyStream(
1354-
request: IncomingMessage
1355-
): ReadableStreamPolyfill<Uint8Array> {
1363+
function requestToBodyStream(request: IncomingMessage): BodyStream {
13561364
const transform = new TransformStream<Uint8Array, Uint8Array>({
13571365
start(controller) {
13581366
request.on('data', (chunk) => controller.enqueue(chunk))
@@ -1361,21 +1369,61 @@ function requestToBodyStream(
13611369
},
13621370
})
13631371

1364-
return transform.readable
1372+
return transform.readable as unknown as ReadableStream<Uint8Array>
13651373
}
13661374

13671375
/**
13681376
* A simple utility to take an original stream and have
13691377
* an API to duplicate it without closing it or mutate any variables
13701378
*/
1371-
function teeableStream<T>(originalStream: ReadableStreamPolyfill<T>): {
1372-
duplicate(): ReadableStreamPolyfill<T>
1379+
function teeableStream<T>(originalStream: ReadableStream<T>): {
1380+
duplicate(): ReadableStream<T>
1381+
original(): ReadableStream<T>
13731382
} {
13741383
return {
13751384
duplicate() {
13761385
const [stream1, stream2] = originalStream.tee()
13771386
originalStream = stream1
13781387
return stream2
13791388
},
1389+
original() {
1390+
return originalStream
1391+
},
13801392
}
13811393
}
1394+
1395+
function bodyStreamToNodeStream(bodyStream: BodyStream): NodeStreams.Readable {
1396+
const reader = bodyStream.getReader()
1397+
return NodeStreams.Readable.from(
1398+
(async function* () {
1399+
while (true) {
1400+
const { done, value } = await reader.read()
1401+
if (done) {
1402+
return
1403+
}
1404+
yield value
1405+
}
1406+
})()
1407+
)
1408+
}
1409+
1410+
function enhanceIncomingMessage<T extends IncomingMessage>(
1411+
base: T,
1412+
body: BodyStream
1413+
): T {
1414+
const stream = bodyStreamToNodeStream(body)
1415+
return new Proxy<T>(base, {
1416+
get(target, name) {
1417+
if (name in stream) {
1418+
const v = stream[name]
1419+
if (typeof v === 'function') {
1420+
return v.bind(stream)
1421+
} else {
1422+
return v
1423+
}
1424+
}
1425+
1426+
return target[name]
1427+
},
1428+
})
1429+
}

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)