diff --git a/.changeset/mean-peas-visit.md b/.changeset/mean-peas-visit.md
new file mode 100644
index 00000000000..8695d257573
--- /dev/null
+++ b/.changeset/mean-peas-visit.md
@@ -0,0 +1,5 @@
+---
+'@vercel/next': patch
+---
+
+ensure unmatched action rewrites are routed to correct handler
diff --git a/packages/next/src/server-build.ts b/packages/next/src/server-build.ts
index 199fed51d6f..0ddf47cbaf6 100644
--- a/packages/next/src/server-build.ts
+++ b/packages/next/src/server-build.ts
@@ -2089,6 +2089,25 @@ export async function serverBuild({
// with that routing section
...afterFilesRewrites,
+ // Ensure that after we normalize `afterFilesRewrites`, unmatched actions are routed to the correct handler
+ // e.g. /foo/.action -> /foo.action. This should only ever match in cases where we're routing to an action handler
+ // and the rewrite normalization led to something like /foo/$1$rscsuff, and $1 had no match.
+ // This is meant to have parity with the .rsc handling below.
+ ...(hasActionOutputSupport
+ ? [
+ {
+ src: `${path.posix.join('/', entryDirectory, '/\\.action$')}`,
+ dest: `${path.posix.join('/', entryDirectory, '/index.action')}`,
+ check: true,
+ },
+ {
+ src: `${path.posix.join('/', entryDirectory, '(.+)/\\.action$')}`,
+ dest: `${path.posix.join('/', entryDirectory, '$1.action')}`,
+ check: true,
+ },
+ ]
+ : []),
+
// ensure non-normalized /.rsc from rewrites is handled
...(appPathRoutesManifest
? [
diff --git a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/edge/static/page.js b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/edge/static/page.js
new file mode 100644
index 00000000000..70a73e7f203
--- /dev/null
+++ b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/edge/static/page.js
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState } from "react";
+import { increment } from "../../actions";
+
+export default function Home() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+ {count}
+
+ Static
+
+ );
+}
diff --git a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/page.js b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/page.js
new file mode 100644
index 00000000000..60199edbbb8
--- /dev/null
+++ b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/page.js
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState } from "react";
+import { increment } from "./actions";
+
+export default function Home() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+ {count}
+
+ Static
+
+ );
+}
diff --git a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/static/page.js b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/static/page.js
new file mode 100644
index 00000000000..d4dca276762
--- /dev/null
+++ b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/static/page.js
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState } from "react";
+import { increment } from "../actions";
+
+export default function Home() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+ {count}
+
+ Static
+
+ );
+}
diff --git a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
index 494ebe14fbe..7ffe4905c0f 100644
--- a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
+++ b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
@@ -293,6 +293,43 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.headers.get('x-edge-runtime')).toBe('1');
}
});
+
+ it('should work when a rewrite greedy matches an action rewrite', async () => {
+ const targetPath = `${basePath}/static`;
+ const canonicalPath = `/greedy-rewrite/${basePath}/static`;
+ const actionId = findActionId(targetPath, runtime);
+
+ const res = await fetch(
+ `${ctx.deploymentUrl}${canonicalPath}`,
+ generateFormDataPayload(actionId)
+ );
+
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('x-matched-path')).toBe(targetPath + '.action');
+ expect(res.headers.get('content-type')).toBe('text/x-component');
+ if (runtime === 'node') {
+ expect(res.headers.get('x-vercel-cache')).toBe('MISS');
+ } else {
+ expect(res.headers.get('x-edge-runtime')).toBe('1');
+ }
+ });
+ });
+
+ describe('rewrite to index', () => {
+ it('should work when user has a rewrite to the index route', async () => {
+ const canonicalPath = '/rewritten-to-index';
+ const actionId = findActionId('', 'node');
+
+ const res = await fetch(
+ `${ctx.deploymentUrl}${canonicalPath}`,
+ generateFormDataPayload(actionId)
+ );
+
+ expect(res.status).toEqual(200);
+ expect(res.headers.get('x-matched-path')).toBe('/index.action');
+ expect(res.headers.get('content-type')).toBe('text/x-component');
+ expect(res.headers.get('x-vercel-cache')).toBe('MISS');
+ });
});
describe('pages', () => {
diff --git a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
index 02977f3ac59..fd6541b69a1 100644
--- a/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
+++ b/packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
@@ -9,6 +9,18 @@ module.exports = {
source: '/rewrite/edge/rsc/static',
destination: '/edge/rsc/static',
},
+ {
+ source: '/greedy-rewrite/static/:path*',
+ destination: '/static/:path*',
+ },
+ {
+ source: '/greedy-rewrite/edge/static/:path*',
+ destination: '/edge/static/:path*',
+ },
+ {
+ source: '/rewritten-to-index',
+ destination: '/?fromRewrite=1',
+ },
];
},
};