Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[next] Only rewrite next-action requests to .action handlers #11504

Merged
merged 12 commits into from Apr 30, 2024
6 changes: 6 additions & 0 deletions .changeset/big-turkeys-smoke.md
@@ -0,0 +1,6 @@
---
"@vercel/next": patch
"api": patch
TooTallNate marked this conversation as resolved.
Show resolved Hide resolved
---

[next] Only rewrite next-action requests to `.action` handlers
TooTallNate marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 0 additions & 30 deletions packages/next/src/server-build.ts
Expand Up @@ -1955,19 +1955,6 @@ export async function serverBuild({
continue: true,
override: true,
},
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
dest: path.posix.join('/', entryDirectory, '/index.action'),
has: [
{
type: 'header',
key: 'content-type',
value: 'multipart/form-data;.*',
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
Expand All @@ -1984,23 +1971,6 @@ export async function serverBuild({
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/((?!.+\\.action).+?)(?:/)?$'
)}`,
dest: path.posix.join('/', entryDirectory, '/$1.action'),
has: [
{
type: 'header',
key: 'content-type',
value: 'multipart/form-data;.*',
},
],
continue: true,
override: true,
},
]
: []),
{
Expand Down
@@ -0,0 +1,45 @@
'use client';

// @ts-ignore
import { useCallback, useState } from 'react';

function request(method) {
return fetch('/api/test', {
method,
headers: {
'content-type': 'multipart/form-data;.*',
},
});
}

export default function Home() {
const [result, setResult] = useState('Press submit');
const onClick = useCallback(async method => {
const res = await request(method);
const text = await res.text();

setResult(res.ok ? `${method} ${text}` : 'Error: ' + res.status);
}, []);

return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex flex-row space-x-2 items-center justify-center">
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('GET')}
>
Submit GET
</button>
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('POST')}
>
Submit POST
</button>
</div>
<div className="text-white">{result}</div>
</div>
</>
);
}
46 changes: 46 additions & 0 deletions packages/next/test/fixtures/00-app-dir-actions/index.test.js
Expand Up @@ -82,6 +82,25 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});

it('should bypass the static cache for a multipart request (no action header)', async () => {
const path = '/client/static';
const actionId = findActionId(path);

const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});

expect(res.status).toEqual(200);
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-matched-path')).toBe(path);
});

it('should properly invoke the action on a dynamic page', async () => {
const path = '/client/dynamic/[id]';
const actionId = findActionId(path);
Expand Down Expand Up @@ -185,4 +204,31 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
});
});
});

describe('pages', () => {
it('should not attempt to rewrite the action path for a server action (POST)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`, {
method: 'POST',
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});

it('should not attempt to rewrite the action path for a server action (GET)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`);

expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});
});
});
@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js!' });
}