From 852d0bb3852a9f97e2b56db997e297ad5e723f19 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Sun, 13 Feb 2022 16:42:05 +0200 Subject: [PATCH 1/7] Allow reading request bodies in middlewares --- packages/next/server/next-server.ts | 44 +++++- packages/next/server/web/adapter.ts | 1 + packages/next/server/web/types.ts | 2 + packages/next/types/misc.d.ts | 5 + .../index.test.ts | 127 ++++++++++++++++++ yarn.lock | 3 +- 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 test/production/reading-request-body-in-middleware/index.test.ts diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 4c7779481822..18e821b95f0d 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -8,6 +8,8 @@ import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' import type { PrerenderManifest } from '../build' import type { Rewrite } from '../lib/load-custom-routes' import type { BaseNextRequest, BaseNextResponse } from './base-http' +import type { ReadableStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' +import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import { execOnce } from '../shared/lib/utils' import { @@ -1236,6 +1238,11 @@ export default class NextNodeServer extends BaseServer { const allHeaders = new Headers() let result: FetchEventResult | null = null + const method = (params.request.method || 'GET').toUpperCase() + let originalBody = + method !== 'GET' && method !== 'HEAD' + ? teeableStream(requestToBodyStream(params.request.body)) + : undefined for (const middleware of this.middleware || []) { if (middleware.match(params.parsedUrl.pathname)) { @@ -1245,6 +1252,7 @@ export default class NextNodeServer extends BaseServer { } await this.ensureMiddleware(middleware.page, middleware.ssr) + const currentBody = originalBody?.duplicate() const middlewareInfo = this.getMiddlewareInfo(middleware.page) @@ -1254,7 +1262,7 @@ export default class NextNodeServer extends BaseServer { env: middlewareInfo.env, request: { headers: params.request.headers, - method: params.request.method || 'GET', + method, nextConfig: { basePath: this.nextConfig.basePath, i18n: this.nextConfig.i18n, @@ -1262,6 +1270,7 @@ export default class NextNodeServer extends BaseServer { }, url: url, page: page, + body: currentBody, }, useCache: !this.nextConfig.experimental.runtime, onWarning: (warning: Error) => { @@ -1334,3 +1343,36 @@ export default class NextNodeServer extends BaseServer { } } } + +/** + * Creates a ReadableStream from a Node.js HTTP request + */ +function requestToBodyStream( + request: IncomingMessage +): ReadableStream { + const transform = new TransformStream({ + start(controller) { + request.on('data', (chunk) => controller.enqueue(chunk)) + request.on('end', () => controller.terminate()) + request.on('error', (err) => controller.error(err)) + }, + }) + + return transform.readable +} + +/** + * A simple utility to take an original stream and have + * an API to duplicate it without closing it or mutate any variables + */ +function teeableStream(originalStream: ReadableStream): { + duplicate(): ReadableStream +} { + return { + duplicate() { + const [stream1, stream2] = originalStream.tee() + originalStream = stream1 + return stream2 + }, + } +} diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index ff7f3559453c..e7760db808e4 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -16,6 +16,7 @@ export async function adapter(params: { page: params.page, input: params.request.url, init: { + body: params.request.body as unknown as ReadableStream, geo: params.request.geo, headers: fromNodeHeaders(params.request.headers), ip: params.request.ip, diff --git a/packages/next/server/web/types.ts b/packages/next/server/web/types.ts index 5a6d48fde63b..a8315e74e5dc 100644 --- a/packages/next/server/web/types.ts +++ b/packages/next/server/web/types.ts @@ -1,4 +1,5 @@ import type { I18NConfig } from '../config-shared' +import type { ReadableStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import type { NextRequest } from '../web/spec-extension/request' import type { NextFetchEvent } from '../web/spec-extension/fetch-event' import type { NextResponse } from './spec-extension/response' @@ -39,6 +40,7 @@ export interface RequestData { params?: { [key: string]: string } } url: string + body?: ReadableStream } export interface FetchEventResult { diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 555d3ad83d6e..8f283c6606f1 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -331,6 +331,11 @@ declare module 'next/dist/compiled/comment-json' { export = m } +declare module 'next/dist/compiled/web-streams-polyfill/ponyfill' { + import m from 'web-streams-polyfill/ponyfill' + export = m +} + declare module 'pnp-webpack-plugin' { import webpack from 'webpack4' diff --git a/test/production/reading-request-body-in-middleware/index.test.ts b/test/production/reading-request-body-in-middleware/index.test.ts new file mode 100644 index 000000000000..c0c54eef74e6 --- /dev/null +++ b/test/production/reading-request-body-in-middleware/index.test.ts @@ -0,0 +1,127 @@ +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: { + 'src/readBody.js': ` + export async function readBody(reader, input = reader.read(), body = "") { + const { value, done } = await input; + const inputText = new TextDecoder().decode(value); + body += inputText; + if (done) { + return body; + } + const next = await reader.read(); + return readBody(reader, next, body); + } + `, + + 'pages/_middleware.js': ` + const { NextResponse } = require('next/server'); + import { readBody } from '../src/readBody'; + + export default async function middleware(request) { + if (!request.body) { + return new Response('No body', { status: 400 }); + } + + const reader = await request.body.getReader(); + const body = await readBody(reader); + const json = JSON.parse(body); + + if (request.nextUrl.searchParams.has("next")) { + return NextResponse.next(); + } + + return new Response(JSON.stringify({ + root: true, + ...json, + }), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }) + } + `, + + 'pages/nested/_middleware.js': ` + const { NextResponse } = require('next/server'); + import { readBody } from '../../src/readBody'; + + export default async function middleware(request) { + if (!request.body) { + return new Response('No body', { status: 400 }); + } + + const reader = await request.body.getReader(); + const body = await readBody(reader); + const json = JSON.parse(body); + + return new Response(JSON.stringify({ + root: false, + ...json, + }), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }) + } + `, + }, + 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, + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index af18b6562100..a5d0874165bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20812,8 +20812,7 @@ webpack-bundle-analyzer@4.3.0: source-list-map "^2.0.0" source-map "~0.6.1" -"webpack-sources3@npm:webpack-sources@3.2.3", webpack-sources@^3.2.3: - name webpack-sources3 +"webpack-sources3@npm:webpack-sources@3.2.3", 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== From 09aaced07f3f6cedb846c35bd2660eb6822fd0dd Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 14 Feb 2022 16:39:25 +0200 Subject: [PATCH 2/7] use Request.json --- .../index.test.ts | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/test/production/reading-request-body-in-middleware/index.test.ts b/test/production/reading-request-body-in-middleware/index.test.ts index c0c54eef74e6..1b43963bf2a8 100644 --- a/test/production/reading-request-body-in-middleware/index.test.ts +++ b/test/production/reading-request-body-in-middleware/index.test.ts @@ -8,31 +8,15 @@ describe('reading request body in middleware', () => { beforeAll(async () => { next = await createNext({ files: { - 'src/readBody.js': ` - export async function readBody(reader, input = reader.read(), body = "") { - const { value, done } = await input; - const inputText = new TextDecoder().decode(value); - body += inputText; - if (done) { - return body; - } - const next = await reader.read(); - return readBody(reader, next, body); - } - `, - 'pages/_middleware.js': ` const { NextResponse } = require('next/server'); - import { readBody } from '../src/readBody'; export default async function middleware(request) { if (!request.body) { return new Response('No body', { status: 400 }); } - const reader = await request.body.getReader(); - const body = await readBody(reader); - const json = JSON.parse(body); + const json = await request.json(); if (request.nextUrl.searchParams.has("next")) { return NextResponse.next(); @@ -52,16 +36,13 @@ describe('reading request body in middleware', () => { 'pages/nested/_middleware.js': ` const { NextResponse } = require('next/server'); - import { readBody } from '../../src/readBody'; export default async function middleware(request) { if (!request.body) { return new Response('No body', { status: 400 }); } - const reader = await request.body.getReader(); - const body = await readBody(reader); - const json = JSON.parse(body); + const json = await request.json(); return new Response(JSON.stringify({ root: false, From a86e6cd74ea43f134adac7cfc645be5aecf56f6f Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 14 Feb 2022 17:57:10 +0200 Subject: [PATCH 3/7] Use the global web type --- packages/next/server/next-server.ts | 10 +++++----- packages/next/server/web/adapter.ts | 2 +- packages/next/server/web/types.ts | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 18e821b95f0d..3f582a3f6c26 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -8,7 +8,7 @@ import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' import type { PrerenderManifest } from '../build' import type { Rewrite } from '../lib/load-custom-routes' import type { BaseNextRequest, BaseNextResponse } from './base-http' -import type { ReadableStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' +import type { ReadableStream as ReadableStreamPolyfill } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import { execOnce } from '../shared/lib/utils' @@ -1270,7 +1270,7 @@ export default class NextNodeServer extends BaseServer { }, url: url, page: page, - body: currentBody, + body: currentBody as unknown as ReadableStream, }, useCache: !this.nextConfig.experimental.runtime, onWarning: (warning: Error) => { @@ -1349,7 +1349,7 @@ export default class NextNodeServer extends BaseServer { */ function requestToBodyStream( request: IncomingMessage -): ReadableStream { +): ReadableStreamPolyfill { const transform = new TransformStream({ start(controller) { request.on('data', (chunk) => controller.enqueue(chunk)) @@ -1365,8 +1365,8 @@ function requestToBodyStream( * A simple utility to take an original stream and have * an API to duplicate it without closing it or mutate any variables */ -function teeableStream(originalStream: ReadableStream): { - duplicate(): ReadableStream +function teeableStream(originalStream: ReadableStreamPolyfill): { + duplicate(): ReadableStreamPolyfill } { return { duplicate() { diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index e7760db808e4..6252ea738f5c 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -16,7 +16,7 @@ export async function adapter(params: { page: params.page, input: params.request.url, init: { - body: params.request.body as unknown as ReadableStream, + body: params.request.body, geo: params.request.geo, headers: fromNodeHeaders(params.request.headers), ip: params.request.ip, diff --git a/packages/next/server/web/types.ts b/packages/next/server/web/types.ts index a8315e74e5dc..029a1024d462 100644 --- a/packages/next/server/web/types.ts +++ b/packages/next/server/web/types.ts @@ -1,5 +1,4 @@ import type { I18NConfig } from '../config-shared' -import type { ReadableStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import type { NextRequest } from '../web/spec-extension/request' import type { NextFetchEvent } from '../web/spec-extension/fetch-event' import type { NextResponse } from './spec-extension/response' From 568b54a2354f65471bcac6f6796c8cf82a45707d Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 15 Feb 2022 13:13:32 +0200 Subject: [PATCH 4/7] Try to replace the underlying stream of IncomingMessage after we read it --- packages/next/server/base-http/node.ts | 16 +++-- packages/next/server/next-server.ts | 66 ++++++++++++++++--- .../index.test.ts | 38 ++++++++++- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/packages/next/server/base-http/node.ts b/packages/next/server/base-http/node.ts index 5d5c54ce0064..d1d720d80989 100644 --- a/packages/next/server/base-http/node.ts +++ b/packages/next/server/base-http/node.ts @@ -7,6 +7,11 @@ import { NEXT_REQUEST_META, RequestMeta } from '../request-meta' import { BaseNextRequest, BaseNextResponse } from './index' +type Req = IncomingMessage & { + [NEXT_REQUEST_META]?: RequestMeta + cookies?: NextApiRequestCookies +} + export class NodeNextRequest extends BaseNextRequest { public headers = this._req.headers; @@ -21,12 +26,11 @@ export class NodeNextRequest extends BaseNextRequest { return this._req } - constructor( - private _req: IncomingMessage & { - [NEXT_REQUEST_META]?: RequestMeta - cookies?: NextApiRequestCookies - } - ) { + set originalRequest(value: Req) { + this._req = value + } + + constructor(private _req: Req) { super(_req.method!.toUpperCase(), _req.url!, _req) } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 3f582a3f6c26..a2faff81ae40 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -8,8 +8,8 @@ import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' import type { PrerenderManifest } from '../build' import type { Rewrite } from '../lib/load-custom-routes' import type { BaseNextRequest, BaseNextResponse } from './base-http' -import type { ReadableStream as ReadableStreamPolyfill } from 'next/dist/compiled/web-streams-polyfill/ponyfill' import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' +import NodeStreams from 'stream' import { execOnce } from '../shared/lib/utils' import { @@ -40,7 +40,7 @@ import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' import compression from 'next/dist/compiled/compression' -import Proxy from 'next/dist/compiled/http-proxy' +import HttpProxy from 'next/dist/compiled/http-proxy' import { route } from './router' import { run } from './web/sandbox' @@ -92,6 +92,8 @@ export interface NodeRequestHandler { ): Promise } +type BodyStream = ReadableStream + export default class NextNodeServer extends BaseServer { private imageResponseCache?: ResponseCache @@ -487,7 +489,7 @@ export default class NextNodeServer extends BaseServer { parsedUrl.search = stringifyQuery(req, query) const target = formatUrl(parsedUrl) - const proxy = new Proxy({ + const proxy = new HttpProxy({ target, changeOrigin: true, ignorePath: true, @@ -1307,6 +1309,14 @@ export default class NextNodeServer extends BaseServer { } } + if (originalBody) { + const noderequest = params.request as NodeNextRequest + noderequest.originalRequest = enhanceIncomingMessage( + noderequest.originalRequest, + originalBody.original() + ) + } + return result } @@ -1347,9 +1357,7 @@ export default class NextNodeServer extends BaseServer { /** * Creates a ReadableStream from a Node.js HTTP request */ -function requestToBodyStream( - request: IncomingMessage -): ReadableStreamPolyfill { +function requestToBodyStream(request: IncomingMessage): BodyStream { const transform = new TransformStream({ start(controller) { request.on('data', (chunk) => controller.enqueue(chunk)) @@ -1358,15 +1366,16 @@ function requestToBodyStream( }, }) - return transform.readable + return transform.readable as unknown as ReadableStream } /** * A simple utility to take an original stream and have * an API to duplicate it without closing it or mutate any variables */ -function teeableStream(originalStream: ReadableStreamPolyfill): { - duplicate(): ReadableStreamPolyfill +function teeableStream(originalStream: ReadableStream): { + duplicate(): ReadableStream + original(): ReadableStream } { return { duplicate() { @@ -1374,5 +1383,44 @@ function teeableStream(originalStream: ReadableStreamPolyfill): { originalStream = stream1 return stream2 }, + original() { + return originalStream + }, } } + +function bodyStreamToNodeStream(bodyStream: BodyStream): NodeStreams.Readable { + const reader = bodyStream.getReader() + return NodeStreams.Readable.from( + (async function* () { + while (true) { + const { done, value } = await reader.read() + if (done) { + return + } + yield value + } + })() + ) +} + +function enhanceIncomingMessage( + base: T, + body: BodyStream +): T { + const stream = bodyStreamToNodeStream(body) + return new Proxy(base, { + get(target, name) { + if (name in stream) { + const v = stream[name] + if (typeof v === 'function') { + return v.bind(stream) + } else { + return v + } + } + + return target[name] + }, + }) +} diff --git a/test/production/reading-request-body-in-middleware/index.test.ts b/test/production/reading-request-body-in-middleware/index.test.ts index 1b43963bf2a8..0f1d61ccfa92 100644 --- a/test/production/reading-request-body-in-middleware/index.test.ts +++ b/test/production/reading-request-body-in-middleware/index.test.ts @@ -19,7 +19,9 @@ describe('reading request body in middleware', () => { const json = await request.json(); if (request.nextUrl.searchParams.has("next")) { - return NextResponse.next(); + const res = NextResponse.next(); + res.headers.set('x-from-root-middleware', '1'); + return res; } return new Response(JSON.stringify({ @@ -55,6 +57,15 @@ describe('reading request body in middleware', () => { }) } `, + + 'pages/api/hi.js': ` + export default function hi(req, res) { + res.json({ + ...req.body, + api: true, + }) + } + `, }, dependencies: {}, }) @@ -105,4 +116,29 @@ describe('reading request body in middleware', () => { 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') + }) }) From afb82ec44c70aad4f4f42c474b94d53a56f7aa5c Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 15 Feb 2022 16:44:22 +0200 Subject: [PATCH 5/7] fix types --- packages/next/server/next-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index a2faff81ae40..6a4ae2323b4f 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1412,7 +1412,7 @@ function enhanceIncomingMessage( return new Proxy(base, { get(target, name) { if (name in stream) { - const v = stream[name] + const v = stream[name as keyof NodeStreams.Readable] if (typeof v === 'function') { return v.bind(stream) } else { @@ -1420,7 +1420,7 @@ function enhanceIncomingMessage( } } - return target[name] + return target[name as keyof T] }, }) } From 6e26b05a246462fbc184941b51a9a3d4e15cd592 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Tue, 15 Feb 2022 18:15:22 +0200 Subject: [PATCH 6/7] Override methods instead of using a proxy. And move all implementation details into a new module --- packages/next/server/body-streams.ts | 87 +++++++++++++++++++++++++++ packages/next/server/next-server.ts | 90 ++-------------------------- 2 files changed, 91 insertions(+), 86 deletions(-) create mode 100644 packages/next/server/body-streams.ts diff --git a/packages/next/server/body-streams.ts b/packages/next/server/body-streams.ts new file mode 100644 index 000000000000..e77982232f4a --- /dev/null +++ b/packages/next/server/body-streams.ts @@ -0,0 +1,87 @@ +import type { IncomingMessage } from 'http' +import { Readable } from 'stream' +import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' + +type BodyStream = ReadableStream + +/** + * Creates a ReadableStream from a Node.js HTTP request + */ +function requestToBodyStream(request: IncomingMessage): BodyStream { + const transform = new TransformStream({ + 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 +} + +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( + 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( + 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 + }, + } +} diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 6a4ae2323b4f..b3c1dcb5cb9e 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -8,8 +8,6 @@ import type { ParsedNextUrl } from '../shared/lib/router/utils/parse-next-url' import type { PrerenderManifest } from '../build' import type { Rewrite } from '../lib/load-custom-routes' import type { BaseNextRequest, BaseNextResponse } from './base-http' -import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' -import NodeStreams from 'stream' import { execOnce } from '../shared/lib/utils' import { @@ -75,6 +73,7 @@ import { loadEnvConfig } from '@next/env' import { getCustomRoute } from './server-route-utils' import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring' import ResponseCache from '../server/response-cache' +import { clonableBodyForRequest } from './body-streams' export * from './base-server' @@ -92,8 +91,6 @@ export interface NodeRequestHandler { ): Promise } -type BodyStream = ReadableStream - export default class NextNodeServer extends BaseServer { private imageResponseCache?: ResponseCache @@ -1243,7 +1240,7 @@ export default class NextNodeServer extends BaseServer { const method = (params.request.method || 'GET').toUpperCase() let originalBody = method !== 'GET' && method !== 'HEAD' - ? teeableStream(requestToBodyStream(params.request.body)) + ? clonableBodyForRequest(params.request.body) : undefined for (const middleware of this.middleware || []) { @@ -1254,8 +1251,6 @@ export default class NextNodeServer extends BaseServer { } await this.ensureMiddleware(middleware.page, middleware.ssr) - const currentBody = originalBody?.duplicate() - const middlewareInfo = this.getMiddlewareInfo(middleware.page) result = await run({ @@ -1272,7 +1267,7 @@ export default class NextNodeServer extends BaseServer { }, url: url, page: page, - body: currentBody as unknown as ReadableStream, + body: originalBody?.cloneBodyStream(), }, useCache: !this.nextConfig.experimental.runtime, onWarning: (warning: Error) => { @@ -1309,13 +1304,7 @@ export default class NextNodeServer extends BaseServer { } } - if (originalBody) { - const noderequest = params.request as NodeNextRequest - noderequest.originalRequest = enhanceIncomingMessage( - noderequest.originalRequest, - originalBody.original() - ) - } + originalBody?.finalize() return result } @@ -1353,74 +1342,3 @@ export default class NextNodeServer extends BaseServer { } } } - -/** - * Creates a ReadableStream from a Node.js HTTP request - */ -function requestToBodyStream(request: IncomingMessage): BodyStream { - const transform = new TransformStream({ - 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 -} - -/** - * A simple utility to take an original stream and have - * an API to duplicate it without closing it or mutate any variables - */ -function teeableStream(originalStream: ReadableStream): { - duplicate(): ReadableStream - original(): ReadableStream -} { - return { - duplicate() { - const [stream1, stream2] = originalStream.tee() - originalStream = stream1 - return stream2 - }, - original() { - return originalStream - }, - } -} - -function bodyStreamToNodeStream(bodyStream: BodyStream): NodeStreams.Readable { - const reader = bodyStream.getReader() - return NodeStreams.Readable.from( - (async function* () { - while (true) { - const { done, value } = await reader.read() - if (done) { - return - } - yield value - } - })() - ) -} - -function enhanceIncomingMessage( - base: T, - body: BodyStream -): T { - const stream = bodyStreamToNodeStream(body) - return new Proxy(base, { - get(target, name) { - if (name in stream) { - const v = stream[name as keyof NodeStreams.Readable] - if (typeof v === 'function') { - return v.bind(stream) - } else { - return v - } - } - - return target[name as keyof T] - }, - }) -} From 6012b9f01b6d2e36d36240d2acfac099c74edeaa Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Thu, 17 Feb 2022 12:56:30 +0200 Subject: [PATCH 7/7] Don't use the explicit ponyfill --- packages/next/server/body-streams.ts | 2 +- packages/next/types/misc.d.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/next/server/body-streams.ts b/packages/next/server/body-streams.ts index e77982232f4a..5ce9a0b3abde 100644 --- a/packages/next/server/body-streams.ts +++ b/packages/next/server/body-streams.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from 'http' import { Readable } from 'stream' -import { TransformStream } from 'next/dist/compiled/web-streams-polyfill/ponyfill' +import { TransformStream } from 'next/dist/compiled/web-streams-polyfill' type BodyStream = ReadableStream diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 8f283c6606f1..555d3ad83d6e 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -331,11 +331,6 @@ declare module 'next/dist/compiled/comment-json' { export = m } -declare module 'next/dist/compiled/web-streams-polyfill/ponyfill' { - import m from 'web-streams-polyfill/ponyfill' - export = m -} - declare module 'pnp-webpack-plugin' { import webpack from 'webpack4'