diff --git a/package.json b/package.json index 0834bcc353b64..eae3531a51ad3 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "find-up": "4.1.0", "firebase": "7.14.5", "flat": "5.0.2", + "form-data": "4.0.0", "fs-extra": "9.0.0", "get-port": "5.1.1", "get-port-please": "3.1.1", diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index eafba6c75866d..993d24fc8373f 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -417,6 +417,11 @@ export async function handleAction({ bound = await decodeReply(formData, serverModuleMap) } else { const action = await decodeAction(formData, serverModuleMap) + if (action === null) { + return { + type: 'not-found', + } + } const actionReturnedState = await action() formState = decodeFormState(actionReturnedState, formData) @@ -495,6 +500,11 @@ export async function handleAction({ }) const formData = await fakeRequest.formData() const action = await decodeAction(formData, serverModuleMap) + if (action === null) { + return { + type: 'not-found', + } + } const actionReturnedState = await action() formState = await decodeFormState(actionReturnedState, formData) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d96eec514047..048c19416e4aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: flat: specifier: 5.0.2 version: 5.0.2 + form-data: + specifier: 4.0.0 + version: 4.0.0 fs-extra: specifier: 9.0.0 version: 9.0.0 diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b4698bf084fa1..29efd9108c395 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -409,32 +409,48 @@ createNextDescribe( ) }) - it('should 404 when POSTing an invalid server action', async () => { - const res = await next.fetch('/non-existent-route', { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: 'foo=bar', + describe('invalid server actions', () => { + it('should 404 when POSTing a x-www-form-urlencoded to a non-existent route', async () => { + const res = await next.fetch('/non-existent-route', { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'foo=bar', + }) + + expect(res.status).toBe(404) }) - expect(res.status).toBe(404) - }) + it('should 404 when POSTing a multipart/form-data to a non-existent route', async () => { + // `form-data` must be used with `node-fetch` otherwise the content-type won't be properly + // set as multipart/form-data + const FormData = require('form-data') as typeof import('form-data') + const data = new FormData() + data.append('foo', 'bar') + const res = await next.fetch('/non-existent-route', { + method: 'POST', + body: data, + }) - it('should log a warning when a server action is not found but an id is provided', async () => { - await next.fetch('/server', { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - 'next-action': 'abc123', - }, - body: 'foo=bar', + expect(res.status).toBe(404) }) - await check( - () => next.cliOutput, - /Failed to find Server Action "abc123". This request might be from an older or newer deployment./ - ) + it('should log a warning when a server action is not found but an id is provided', async () => { + await next.fetch('/server', { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'next-action': 'abc123', + }, + body: 'foo=bar', + }) + + await check( + () => next.cliOutput, + /Failed to find Server Action "abc123". This request might be from an older or newer deployment./ + ) + }) }) if (isNextStart) {