From 76f7645ce613bc174c6ae1e25c72444947d161d4 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 13 Mar 2023 11:34:22 -0700 Subject: [PATCH] Update app route handler proxy handling (#47088) This ensures our Proxy wrapping request fields works properly in the edge-runtime by explicitly binding to the correct request instance. x-ref: [slack thread](https://vercel.slack.com/archives/C03UR7US95F/p1678730563467089?thread_ts=1678662292.695769&cid=C03UR7US95F) --- .../route-handlers/app-route-route-handler.ts | 12 ++- .../app-routes/app-custom-routes.test.ts | 75 +++++++++++++++++++ .../app/edge/advanced/body/json/route.ts | 12 +++ .../app/edge/advanced/body/streaming/route.ts | 27 +++++++ .../app/edge/advanced/body/text/route.ts | 12 +++ .../app/edge/advanced/query/route.ts | 14 ++++ 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 test/e2e/app-dir/app-routes/app/edge/advanced/body/json/route.ts create mode 100644 test/e2e/app-dir/app-routes/app/edge/advanced/body/streaming/route.ts create mode 100644 test/e2e/app-dir/app-routes/app/edge/advanced/body/text/route.ts create mode 100644 test/e2e/app-dir/app-routes/app/edge/advanced/query/route.ts diff --git a/packages/next/src/server/future/route-handlers/app-route-route-handler.ts b/packages/next/src/server/future/route-handlers/app-route-route-handler.ts index 9aecdd84a8658..ad38e6520b6cc 100644 --- a/packages/next/src/server/future/route-handlers/app-route-route-handler.ts +++ b/packages/next/src/server/future/route-handlers/app-route-route-handler.ts @@ -273,8 +273,12 @@ function proxyRequest(req: NextRequest, module: AppRouteModule): NextRequest { const result = handleForceStatic(target.href, prop) if (result !== undefined) return result } + const value = (target as any)[prop] - return (target as any)[prop] + if (typeof value === 'function') { + return value.bind(target) + } + return value }, set(target, prop, value) { handleNextUrlBailout(prop) @@ -320,8 +324,12 @@ function proxyRequest(req: NextRequest, module: AppRouteModule): NextRequest { const result = handleForceStatic(target.url, prop) if (result !== undefined) return result } + const value = (target as any)[prop] - return (target as any)[prop] + if (typeof value === 'function') { + return value.bind(target) + } + return value }, set(target, prop, value) { handleReqBailout(prop) diff --git a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts index 2c4ccf150a32f..3ed6e9ac5d42d 100644 --- a/test/e2e/app-dir/app-routes/app-custom-routes.test.ts +++ b/test/e2e/app-dir/app-routes/app-custom-routes.test.ts @@ -138,6 +138,14 @@ createNextDescribe( const meta = getRequestMeta(res.headers) expect(meta.ping).toEqual('pong') }) + + it('can read query parameters (edge)', async () => { + const res = await next.fetch('/edge/advanced/query?ping=pong') + + expect(res.status).toEqual(200) + const meta = getRequestMeta(res.headers) + expect(meta.ping).toEqual('pong') + }) }) describe('response', () => { @@ -203,6 +211,27 @@ createNextDescribe( }) } + it('can handle handle a streaming request and streaming response (edge)', async () => { + const body = new Array(10).fill(JSON.stringify({ ping: 'pong' })) + let index = 0 + const stream = new Readable({ + read() { + if (index >= body.length) return this.push(null) + + this.push(body[index] + '\n') + index++ + }, + }) + + const res = await next.fetch('/edge/advanced/body/streaming', { + method: 'POST', + body: stream, + }) + + expect(res.status).toEqual(200) + expect(await res.text()).toEqual(body.join('\n') + '\n') + }) + it('can read a JSON encoded body', async () => { const body = { ping: 'pong' } const res = await next.fetch('/advanced/body/json', { @@ -215,6 +244,18 @@ createNextDescribe( expect(meta.body).toEqual(body) }) + it('can read a JSON encoded body (edge)', async () => { + const body = { ping: 'pong' } + const res = await next.fetch('/edge/advanced/body/json', { + method: 'POST', + body: JSON.stringify(body), + }) + + expect(res.status).toEqual(200) + const meta = getRequestMeta(res.headers) + expect(meta.body).toEqual(body) + }) + // we can't stream a body to a function currently only stream response if (!(global as any).isNextDeploy) { it('can read a streamed JSON encoded body', async () => { @@ -240,6 +281,28 @@ createNextDescribe( }) } + it('can read a streamed JSON encoded body (edge)', async () => { + const body = { ping: 'pong' } + const encoded = JSON.stringify(body) + let index = 0 + const stream = new Readable({ + async read() { + if (index >= encoded.length) return this.push(null) + + this.push(encoded[index]) + index++ + }, + }) + const res = await next.fetch('/edge/advanced/body/json', { + method: 'POST', + body: stream, + }) + + expect(res.status).toEqual(200) + const meta = getRequestMeta(res.headers) + expect(meta.body).toEqual(body) + }) + it('can read the text body', async () => { const body = 'hello, world' const res = await next.fetch('/advanced/body/text', { @@ -251,6 +314,18 @@ createNextDescribe( const meta = getRequestMeta(res.headers) expect(meta.body).toEqual(body) }) + + it('can read the text body (edge)', async () => { + const body = 'hello, world' + const res = await next.fetch('/edge/advanced/body/text', { + method: 'POST', + body, + }) + + expect(res.status).toEqual(200) + const meta = getRequestMeta(res.headers) + expect(meta.body).toEqual(body) + }) }) describe('context', () => { diff --git a/test/e2e/app-dir/app-routes/app/edge/advanced/body/json/route.ts b/test/e2e/app-dir/app-routes/app/edge/advanced/body/json/route.ts new file mode 100644 index 0000000000000..e55869eeba3d9 --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/edge/advanced/body/json/route.ts @@ -0,0 +1,12 @@ +import type { NextRequest } from 'next/server' +import { withRequestMeta } from '../../../../../helpers' + +export const runtime = 'experimental-edge' + +export async function POST(request: NextRequest) { + const body = await request.json() + return new Response('hello, world', { + status: 200, + headers: withRequestMeta({ body }), + }) +} diff --git a/test/e2e/app-dir/app-routes/app/edge/advanced/body/streaming/route.ts b/test/e2e/app-dir/app-routes/app/edge/advanced/body/streaming/route.ts new file mode 100644 index 0000000000000..af98c4c380166 --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/edge/advanced/body/streaming/route.ts @@ -0,0 +1,27 @@ +import type { NextRequest } from 'next/server' + +export const runtime = 'experimental-edge' + +export async function POST(request: NextRequest) { + const reader = request.body?.getReader() + if (!reader) { + return new Response(null, { status: 400, statusText: 'Bad Request' }) + } + + // Readable stream here is polyfilled from the Fetch API (from undici). + const stream = new ReadableStream({ + async pull(controller) { + // Read the next chunk from the stream. + const { value, done } = await reader.read() + if (done) { + // Finish the stream. + return controller.close() + } + + // Add the request value to the response stream. + controller.enqueue(value) + }, + }) + + return new Response(stream, { status: 200 }) +} diff --git a/test/e2e/app-dir/app-routes/app/edge/advanced/body/text/route.ts b/test/e2e/app-dir/app-routes/app/edge/advanced/body/text/route.ts new file mode 100644 index 0000000000000..aa39c6f22529a --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/edge/advanced/body/text/route.ts @@ -0,0 +1,12 @@ +import type { NextRequest } from 'next/server' +import { withRequestMeta } from '../../../../../helpers' + +export const runtime = 'experimental-edge' + +export async function POST(request: NextRequest) { + const body = await request.text() + return new Response('hello, world', { + status: 200, + headers: withRequestMeta({ body }), + }) +} diff --git a/test/e2e/app-dir/app-routes/app/edge/advanced/query/route.ts b/test/e2e/app-dir/app-routes/app/edge/advanced/query/route.ts new file mode 100644 index 0000000000000..15ed371f6848e --- /dev/null +++ b/test/e2e/app-dir/app-routes/app/edge/advanced/query/route.ts @@ -0,0 +1,14 @@ +import { withRequestMeta } from '../../../../helpers' +import { NextRequest } from 'next/server' + +export const runtime = 'experimental-edge' + +export async function GET(request: NextRequest): Promise { + const { searchParams } = request.nextUrl + + return new Response('hello, world', { + headers: withRequestMeta({ + ping: searchParams.get('ping'), + }), + }) +}