diff --git a/package.json b/package.json index ee7fcf8a5507..14cf4fdf58db 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@babel/preset-flow": "7.14.5", "@babel/preset-react": "7.14.5", "@edge-runtime/jest-environment": "1.1.0-beta.36", + "@edge-runtime/primitives": "1.1.0-beta.36", "@fullhuman/postcss-purgecss": "1.3.0", "@mdx-js/loader": "0.18.0", "@next/bundle-analyzer": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36240fedaebc..8f1d6a804ce8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,7 @@ importers: '@babel/preset-flow': 7.14.5 '@babel/preset-react': 7.14.5 '@edge-runtime/jest-environment': 1.1.0-beta.36 + '@edge-runtime/primitives': 1.1.0-beta.36 '@fullhuman/postcss-purgecss': 1.3.0 '@mdx-js/loader': 0.18.0 '@next/bundle-analyzer': workspace:* @@ -176,6 +177,7 @@ importers: '@babel/preset-flow': 7.14.5_@babel+core@7.18.0 '@babel/preset-react': 7.14.5_@babel+core@7.18.0 '@edge-runtime/jest-environment': 1.1.0-beta.36 + '@edge-runtime/primitives': 1.1.0-beta.36 '@fullhuman/postcss-purgecss': 1.3.0 '@mdx-js/loader': 0.18.0_uuaxwgga6hqycsez5ok7v2wg4i '@next/bundle-analyzer': link:packages/next-bundle-analyzer @@ -4025,6 +4027,13 @@ packages: jest-util: 28.1.3 dev: true + /@edge-runtime/primitives/1.1.0-beta.36: + resolution: + { + integrity: sha512-Tji7SGWmn1+JGSnzFtWUoS7+kODIFprTyIAw0EBOVWEQKWfs7r0aTEm1XkJR0+d1jP9f0GB5LBKG/Z7KFyhx7g==, + } + dev: true + /@edge-runtime/primitives/1.1.0-beta.37: resolution: { diff --git a/test/e2e/edge-can-read-request-body/app/.gitignore b/test/e2e/edge-can-read-request-body/app/.gitignore new file mode 100644 index 000000000000..e985853ed84a --- /dev/null +++ b/test/e2e/edge-can-read-request-body/app/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/test/e2e/edge-can-read-request-body/app/middleware.js b/test/e2e/edge-can-read-request-body/app/middleware.js new file mode 100644 index 000000000000..bbddddb07ef8 --- /dev/null +++ b/test/e2e/edge-can-read-request-body/app/middleware.js @@ -0,0 +1,49 @@ +// @ts-check + +import { NextResponse } from 'next/server' + +/** + * @param {NextRequest} req + */ +export default async function middleware(req) { + const res = NextResponse.next() + res.headers.set('x-incoming-content-type', req.headers.get('content-type')) + + const handler = + bodyHandlers[req.nextUrl.searchParams.get('middleware-handler')] + const headers = await handler?.(req) + for (const [key, value] of headers ?? []) { + res.headers.set(key, value) + } + + return res +} + +/** + * @typedef {import('next/server').NextRequest} NextRequest + * @typedef {(req: NextRequest) => Promise<[string, string][]>} Handler + * @type {Record} + */ +const bodyHandlers = { + json: async (req) => { + const json = await req.json() + return [ + ['x-req-type', 'json'], + ['x-serialized', JSON.stringify(json)], + ] + }, + text: async (req) => { + const text = await req.text() + return [ + ['x-req-type', 'text'], + ['x-serialized', text], + ] + }, + formData: async (req) => { + const formData = await req.formData() + return [ + ['x-req-type', 'formData'], + ['x-serialized', JSON.stringify(Object.fromEntries(formData))], + ] + }, +} diff --git a/test/e2e/edge-can-read-request-body/app/package.json b/test/e2e/edge-can-read-request-body/app/package.json new file mode 100644 index 000000000000..8354a6a3f234 --- /dev/null +++ b/test/e2e/edge-can-read-request-body/app/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "next": "canary", + "react": "latest", + "react-dom": "latest" + } +} diff --git a/test/e2e/edge-can-read-request-body/app/pages/api/nothing.js b/test/e2e/edge-can-read-request-body/app/pages/api/nothing.js new file mode 100644 index 000000000000..7c595baecad2 --- /dev/null +++ b/test/e2e/edge-can-read-request-body/app/pages/api/nothing.js @@ -0,0 +1,3 @@ +export default (_req, res) => { + res.send('ok') +} diff --git a/test/e2e/edge-can-read-request-body/index.test.ts b/test/e2e/edge-can-read-request-body/index.test.ts new file mode 100644 index 000000000000..9b535001f219 --- /dev/null +++ b/test/e2e/edge-can-read-request-body/index.test.ts @@ -0,0 +1,109 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' +import { FormData, fetch } from '@edge-runtime/primitives' +import path from 'path' + +async function serialize(response: Response) { + return { + text: await response.text(), + headers: Object.fromEntries(response.headers), + status: response.status, + } +} + +describe('Edge can read request body', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(path.resolve(__dirname, './app')), + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + it('renders the static page', async () => { + const html = await renderViaHTTP(next.url, '/api/nothing') + expect(html).toContain('ok') + }) + + describe('middleware', () => { + it('reads a JSON body', async () => { + const response = await fetch( + `${next.url}/api/nothing?middleware-handler=json`, + { + method: 'POST', + body: JSON.stringify({ hello: 'world' }), + } + ) + expect(await serialize(response)).toMatchObject({ + text: expect.stringContaining('ok'), + status: 200, + headers: { + 'x-req-type': 'json', + 'x-serialized': '{"hello":"world"}', + }, + }) + }) + + it('reads a text body', async () => { + const response = await fetch( + `${next.url}/api/nothing?middleware-handler=text`, + { + method: 'POST', + body: JSON.stringify({ hello: 'world' }), + } + ) + expect(await serialize(response)).toMatchObject({ + text: expect.stringContaining('ok'), + status: 200, + headers: { + 'x-req-type': 'text', + 'x-serialized': '{"hello":"world"}', + }, + }) + }) + + it('reads an URL encoded form data', async () => { + const response = await fetch( + `${next.url}/api/nothing?middleware-handler=formData`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ hello: 'world' }).toString(), + } + ) + expect(await serialize(response)).toMatchObject({ + text: expect.stringContaining('ok'), + status: 200, + headers: { + 'x-req-type': 'formData', + 'x-serialized': '{"hello":"world"}', + }, + }) + }) + + it('reads a multipart form data', async () => { + const formData = new FormData() + formData.set('hello', 'world') + const response = await fetch( + `${next.url}/api/nothing?middleware-handler=formData`, + { + method: 'POST', + body: formData, + } + ) + expect(await serialize(response)).toMatchObject({ + text: expect.stringContaining('ok'), + status: 200, + headers: { + 'x-req-type': 'formData', + 'x-serialized': '{"hello":"world"}', + }, + }) + }) + }) +})