-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow reading request bodies in middlewares (#34294)
Related: - resolves #30953
- Loading branch information
Showing
7 changed files
with
256 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import type { IncomingMessage } from 'http' | ||
import { Readable } from 'stream' | ||
import { TransformStream } from 'next/dist/compiled/web-streams-polyfill' | ||
|
||
type BodyStream = ReadableStream<Uint8Array> | ||
|
||
/** | ||
* Creates a ReadableStream from a Node.js HTTP request | ||
*/ | ||
function requestToBodyStream(request: IncomingMessage): BodyStream { | ||
const transform = new TransformStream<Uint8Array, Uint8Array>({ | ||
start(controller) { | ||
request.on('data', (chunk) => controller.enqueue(chunk)) | ||
request.on('end', () => controller.terminate()) | ||
request.on('error', (err) => controller.error(err)) | ||
}, | ||
}) | ||
|
||
return transform.readable as unknown as ReadableStream<Uint8Array> | ||
} | ||
|
||
function bodyStreamToNodeStream(bodyStream: BodyStream): Readable { | ||
const reader = bodyStream.getReader() | ||
return Readable.from( | ||
(async function* () { | ||
while (true) { | ||
const { done, value } = await reader.read() | ||
if (done) { | ||
return | ||
} | ||
yield value | ||
} | ||
})() | ||
) | ||
} | ||
|
||
function replaceRequestBody<T extends IncomingMessage>( | ||
base: T, | ||
stream: Readable | ||
): T { | ||
for (const key in stream) { | ||
let v = stream[key as keyof Readable] as any | ||
if (typeof v === 'function') { | ||
v = v.bind(stream) | ||
} | ||
base[key as keyof T] = v | ||
} | ||
|
||
return base | ||
} | ||
|
||
/** | ||
* An interface that encapsulates body stream cloning | ||
* of an incoming request. | ||
*/ | ||
export function clonableBodyForRequest<T extends IncomingMessage>( | ||
incomingMessage: T | ||
) { | ||
let bufferedBodyStream: BodyStream | null = null | ||
|
||
return { | ||
/** | ||
* Replaces the original request body if necessary. | ||
* This is done because once we read the body from the original request, | ||
* we can't read it again. | ||
*/ | ||
finalize(): void { | ||
if (bufferedBodyStream) { | ||
replaceRequestBody( | ||
incomingMessage, | ||
bodyStreamToNodeStream(bufferedBodyStream) | ||
) | ||
} | ||
}, | ||
/** | ||
* Clones the body stream | ||
* to pass into a middleware | ||
*/ | ||
cloneBodyStream(): BodyStream { | ||
const originalStream = | ||
bufferedBodyStream ?? requestToBodyStream(incomingMessage) | ||
const [stream1, stream2] = originalStream.tee() | ||
bufferedBodyStream = stream1 | ||
return stream2 | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
test/production/reading-request-body-in-middleware/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { createNext } from 'e2e-utils' | ||
import { NextInstance } from 'test/lib/next-modes/base' | ||
import { fetchViaHTTP } from 'next-test-utils' | ||
|
||
describe('reading request body in middleware', () => { | ||
let next: NextInstance | ||
|
||
beforeAll(async () => { | ||
next = await createNext({ | ||
files: { | ||
'pages/_middleware.js': ` | ||
const { NextResponse } = require('next/server'); | ||
export default async function middleware(request) { | ||
if (!request.body) { | ||
return new Response('No body', { status: 400 }); | ||
} | ||
const json = await request.json(); | ||
if (request.nextUrl.searchParams.has("next")) { | ||
const res = NextResponse.next(); | ||
res.headers.set('x-from-root-middleware', '1'); | ||
return res; | ||
} | ||
return new Response(JSON.stringify({ | ||
root: true, | ||
...json, | ||
}), { | ||
status: 200, | ||
headers: { | ||
'content-type': 'application/json', | ||
}, | ||
}) | ||
} | ||
`, | ||
|
||
'pages/nested/_middleware.js': ` | ||
const { NextResponse } = require('next/server'); | ||
export default async function middleware(request) { | ||
if (!request.body) { | ||
return new Response('No body', { status: 400 }); | ||
} | ||
const json = await request.json(); | ||
return new Response(JSON.stringify({ | ||
root: false, | ||
...json, | ||
}), { | ||
status: 200, | ||
headers: { | ||
'content-type': 'application/json', | ||
}, | ||
}) | ||
} | ||
`, | ||
|
||
'pages/api/hi.js': ` | ||
export default function hi(req, res) { | ||
res.json({ | ||
...req.body, | ||
api: true, | ||
}) | ||
} | ||
`, | ||
}, | ||
dependencies: {}, | ||
}) | ||
}) | ||
afterAll(() => next.destroy()) | ||
|
||
it('rejects with 400 for get requests', async () => { | ||
const response = await fetchViaHTTP(next.url, '/') | ||
expect(response.status).toEqual(400) | ||
}) | ||
|
||
it('returns root: true for root calls', async () => { | ||
const response = await fetchViaHTTP( | ||
next.url, | ||
'/', | ||
{}, | ||
{ | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
foo: 'bar', | ||
}), | ||
} | ||
) | ||
expect(response.status).toEqual(200) | ||
expect(await response.json()).toEqual({ | ||
foo: 'bar', | ||
root: true, | ||
}) | ||
}) | ||
|
||
it('reads the same body on both middlewares', async () => { | ||
const response = await fetchViaHTTP( | ||
next.url, | ||
'/nested/hello', | ||
{ | ||
next: '1', | ||
}, | ||
{ | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
foo: 'bar', | ||
}), | ||
} | ||
) | ||
expect(response.status).toEqual(200) | ||
expect(await response.json()).toEqual({ | ||
foo: 'bar', | ||
root: false, | ||
}) | ||
}) | ||
|
||
it('passes the body to the api endpoint', async () => { | ||
const response = await fetchViaHTTP( | ||
next.url, | ||
'/api/hi', | ||
{ | ||
next: '1', | ||
}, | ||
{ | ||
method: 'POST', | ||
headers: { | ||
'content-type': 'application/json', | ||
}, | ||
body: JSON.stringify({ | ||
foo: 'bar', | ||
}), | ||
} | ||
) | ||
expect(response.status).toEqual(200) | ||
expect(await response.json()).toEqual({ | ||
foo: 'bar', | ||
api: true, | ||
}) | ||
expect(response.headers.get('x-from-root-middleware')).toEqual('1') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20812,8 +20812,7 @@ [email protected]: | |
source-list-map "^2.0.0" | ||
source-map "~0.6.1" | ||
|
||
"webpack-sources3@npm:[email protected]", webpack-sources@^3.2.3: | ||
name webpack-sources3 | ||
"webpack-sources3@npm:[email protected]", webpack-sources@^3.2.2, webpack-sources@^3.2.3: | ||
version "3.2.3" | ||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" | ||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== | ||
|