Skip to content

Commit 0a841a5

Browse files
committed
Allow reading request bodies in middlewares
1 parent 7be6359 commit 0a841a5

File tree

6 files changed

+179
-3
lines changed

6 files changed

+179
-3
lines changed

packages/next/server/next-server.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +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 } from 'next/dist/compiled/web-streams-polyfill/ponyfill'
12+
import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill'
1113

1214
import { execOnce } from '../shared/lib/utils'
1315
import {
@@ -1239,6 +1241,11 @@ export default class NextNodeServer extends BaseServer {
12391241

12401242
const allHeaders = new Headers()
12411243
let result: FetchEventResult | null = null
1244+
const method = (params.request.method || 'GET').toUpperCase()
1245+
let originalBody =
1246+
method !== 'GET' && method !== 'HEAD'
1247+
? teeableStream(requestToBodyStream(params.request.body))
1248+
: undefined
12421249

12431250
for (const middleware of this.middleware || []) {
12441251
if (middleware.match(params.parsedUrl.pathname)) {
@@ -1248,6 +1255,7 @@ export default class NextNodeServer extends BaseServer {
12481255
}
12491256

12501257
await this.ensureMiddleware(middleware.page, middleware.ssr)
1258+
const currentBody = originalBody?.duplicate()
12511259

12521260
const middlewareInfo = this.getMiddlewareInfo(middleware.page)
12531261

@@ -1257,14 +1265,15 @@ export default class NextNodeServer extends BaseServer {
12571265
env: middlewareInfo.env,
12581266
request: {
12591267
headers: params.request.headers,
1260-
method: params.request.method || 'GET',
1268+
method,
12611269
nextConfig: {
12621270
basePath: this.nextConfig.basePath,
12631271
i18n: this.nextConfig.i18n,
12641272
trailingSlash: this.nextConfig.trailingSlash,
12651273
},
12661274
url: url,
12671275
page: page,
1276+
body: currentBody,
12681277
},
12691278
useCache: !this.nextConfig.experimental.runtime,
12701279
onWarning: (warning: Error) => {
@@ -1337,3 +1346,36 @@ export default class NextNodeServer extends BaseServer {
13371346
}
13381347
}
13391348
}
1349+
1350+
/**
1351+
* Creates a ReadableStream from a Node.js HTTP request
1352+
*/
1353+
function requestToBodyStream(
1354+
request: IncomingMessage
1355+
): ReadableStream<Uint8Array> {
1356+
const transform = new TransformStream<Uint8Array, Uint8Array>({
1357+
start(controller) {
1358+
request.on('data', (chunk) => controller.enqueue(chunk))
1359+
request.on('end', () => controller.terminate())
1360+
request.on('error', (err) => controller.error(err))
1361+
},
1362+
})
1363+
1364+
return transform.readable
1365+
}
1366+
1367+
/**
1368+
* A simple utility to take an original stream and have
1369+
* an API to duplicate it without closing it or mutate any variables
1370+
*/
1371+
function teeableStream<T>(originalStream: ReadableStream<T>): {
1372+
duplicate(): ReadableStream<T>
1373+
} {
1374+
return {
1375+
duplicate() {
1376+
const [stream1, stream2] = originalStream.tee()
1377+
originalStream = stream1
1378+
return stream2
1379+
},
1380+
}
1381+
}

packages/next/server/web/adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export async function adapter(params: {
1616
page: params.page,
1717
input: params.request.url,
1818
init: {
19+
body: params.request.body as unknown as ReadableStream<Uint8Array>,
1920
geo: params.request.geo,
2021
headers: fromNodeHeaders(params.request.headers),
2122
ip: params.request.ip,

packages/next/server/web/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { I18NConfig } from '../config-shared'
2+
import type { ReadableStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill'
23
import type { NextRequest } from '../web/spec-extension/request'
34
import type { NextFetchEvent } from '../web/spec-extension/fetch-event'
45
import type { NextResponse } from './spec-extension/response'
@@ -39,6 +40,7 @@ export interface RequestData {
3940
params?: { [key: string]: string }
4041
}
4142
url: string
43+
body?: ReadableStream<Uint8Array>
4244
}
4345

4446
export interface FetchEventResult {

packages/next/types/misc.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,11 @@ declare module 'next/dist/compiled/comment-json' {
331331
export = m
332332
}
333333

334+
declare module 'next/dist/compiled/web-streams-polyfill/ponyfill' {
335+
import m from 'web-streams-polyfill/ponyfill'
336+
export = m
337+
}
338+
334339
declare module 'pnp-webpack-plugin' {
335340
import webpack from 'webpack4'
336341

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { createNext } from 'e2e-utils'
2+
import { NextInstance } from 'test/lib/next-modes/base'
3+
import { fetchViaHTTP } from 'next-test-utils'
4+
5+
describe('reading request body in middleware', () => {
6+
let next: NextInstance
7+
8+
beforeAll(async () => {
9+
next = await createNext({
10+
files: {
11+
'src/readBody.js': `
12+
export async function readBody(reader, input = reader.read(), body = "") {
13+
const { value, done } = await input;
14+
const inputText = new TextDecoder().decode(value);
15+
body += inputText;
16+
if (done) {
17+
return body;
18+
}
19+
const next = await reader.read();
20+
return readBody(reader, next, body);
21+
}
22+
`,
23+
24+
'pages/_middleware.js': `
25+
const { NextResponse } = require('next/server');
26+
import { readBody } from '../src/readBody';
27+
28+
export default async function middleware(request) {
29+
if (!request.body) {
30+
return new Response('No body', { status: 400 });
31+
}
32+
33+
const reader = await request.body.getReader();
34+
const body = await readBody(reader);
35+
const json = JSON.parse(body);
36+
37+
if (request.nextUrl.searchParams.has("next")) {
38+
return NextResponse.next();
39+
}
40+
41+
return new Response(JSON.stringify({
42+
root: true,
43+
...json,
44+
}), {
45+
status: 200,
46+
headers: {
47+
'content-type': 'application/json',
48+
},
49+
})
50+
}
51+
`,
52+
53+
'pages/nested/_middleware.js': `
54+
const { NextResponse } = require('next/server');
55+
import { readBody } from '../../src/readBody';
56+
57+
export default async function middleware(request) {
58+
if (!request.body) {
59+
return new Response('No body', { status: 400 });
60+
}
61+
62+
const reader = await request.body.getReader();
63+
const body = await readBody(reader);
64+
const json = JSON.parse(body);
65+
66+
return new Response(JSON.stringify({
67+
root: false,
68+
...json,
69+
}), {
70+
status: 200,
71+
headers: {
72+
'content-type': 'application/json',
73+
},
74+
})
75+
}
76+
`,
77+
},
78+
dependencies: {},
79+
})
80+
})
81+
afterAll(() => next.destroy())
82+
83+
it('rejects with 400 for get requests', async () => {
84+
const response = await fetchViaHTTP(next.url, '/')
85+
expect(response.status).toEqual(400)
86+
})
87+
88+
it('returns root: true for root calls', async () => {
89+
const response = await fetchViaHTTP(
90+
next.url,
91+
'/',
92+
{},
93+
{
94+
method: 'POST',
95+
body: JSON.stringify({
96+
foo: 'bar',
97+
}),
98+
}
99+
)
100+
expect(response.status).toEqual(200)
101+
expect(await response.json()).toEqual({
102+
foo: 'bar',
103+
root: true,
104+
})
105+
})
106+
107+
it('reads the same body on both middlewares', async () => {
108+
const response = await fetchViaHTTP(
109+
next.url,
110+
'/nested/hello',
111+
{
112+
next: '1',
113+
},
114+
{
115+
method: 'POST',
116+
body: JSON.stringify({
117+
foo: 'bar',
118+
}),
119+
}
120+
)
121+
expect(response.status).toEqual(200)
122+
expect(await response.json()).toEqual({
123+
foo: 'bar',
124+
root: false,
125+
})
126+
})
127+
})

yarn.lock

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20812,8 +20812,7 @@ [email protected]:
2081220812
source-list-map "^2.0.0"
2081320813
source-map "~0.6.1"
2081420814

20815-
"webpack-sources3@npm:[email protected]", webpack-sources@^3.2.3:
20816-
name webpack-sources3
20815+
"webpack-sources3@npm:[email protected]", webpack-sources@^3.2.2, webpack-sources@^3.2.3:
2081720816
version "3.2.3"
2081820817
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
2081920818
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==

0 commit comments

Comments
 (0)