diff --git a/packages/next/lib/pick.ts b/packages/next/lib/pick.ts new file mode 100644 index 0000000000000..34afb06ec2833 --- /dev/null +++ b/packages/next/lib/pick.ts @@ -0,0 +1,7 @@ +export function pick(obj: T, keys: K[]): Pick { + const newObj = {} as Pick + for (const key of keys) { + newObj[key] = obj[key] + } + return newObj +} diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 4ee2a4161d5c6..2b4ea09ead45b 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -4,6 +4,7 @@ import { EDGE_UNSUPPORTED_NODE_APIS } from '../../../shared/lib/constants' import { EdgeRuntime } from 'next/dist/compiled/edge-runtime' import { readFileSync, promises as fs } from 'fs' import { validateURL } from '../utils' +import { pick } from '../../../lib/pick' const WEBPACK_HASH_REGEX = /__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g @@ -134,6 +135,19 @@ async function createModuleContext(options: ModuleContextOptions) { if (typeof input === 'object' && 'url' in input) { return __fetch(input.url, { + ...pick(input, [ + 'method', + 'body', + 'cache', + 'credentials', + 'integrity', + 'keepalive', + 'mode', + 'redirect', + 'referrer', + 'referrerPolicy', + 'signal', + ]), ...init, headers: { ...Object.fromEntries(input.headers), diff --git a/test/e2e/middleware-fetches-with-any-http-method/index.test.ts b/test/e2e/middleware-fetches-with-any-http-method/index.test.ts new file mode 100644 index 0000000000000..fd8be9a47f730 --- /dev/null +++ b/test/e2e/middleware-fetches-with-any-http-method/index.test.ts @@ -0,0 +1,91 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' + +describe('Middleware fetches with any HTTP method', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/api/ping.js': ` + export default (req, res) => { + res.send(JSON.stringify({ + method: req.method, + headers: {...req.headers}, + })) + } + `, + 'middleware.js': ` + import { NextResponse } from 'next/server'; + + const HTTP_ECHO_URL = 'https://http-echo-kou029w.vercel.app/'; + + export default async (req) => { + const kind = req.nextUrl.searchParams.get('kind') + const handler = handlers[kind] ?? handlers['normal-fetch']; + + const response = await handler({url: HTTP_ECHO_URL, method: req.method}); + const json = await response.text() + + const res = NextResponse.next(); + res.headers.set('x-resolved', json ?? '{}'); + return res + } + + const handlers = { + 'new-request': ({url, method}) => + fetch(new Request(url, { method, headers: { 'x-kind': 'new-request' } })), + + 'normal-fetch': ({url, method}) => + fetch(url, { method, headers: { 'x-kind': 'normal-fetch' } }) + } + `, + }, + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + it('passes the method on a direct fetch request', async () => { + const response = await fetchViaHTTP( + next.url, + '/api/ping', + {}, + { method: 'POST' } + ) + const json = await response.json() + expect(json).toMatchObject({ + method: 'POST', + }) + + const headerJson = JSON.parse(response.headers.get('x-resolved')) + expect(headerJson).toMatchObject({ + method: 'POST', + headers: { + 'x-kind': 'normal-fetch', + }, + }) + }) + + it('passes the method when providing a Request object', async () => { + const response = await fetchViaHTTP( + next.url, + '/api/ping', + { kind: 'new-request' }, + { method: 'POST' } + ) + const json = await response.json() + expect(json).toMatchObject({ + method: 'POST', + }) + + const headerJson = JSON.parse(response.headers.get('x-resolved')) + expect(headerJson).toMatchObject({ + method: 'POST', + headers: { + 'x-kind': 'new-request', + }, + }) + }) +})