diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx
index 9a8a45e8dea5c..005bc657faa3b 100644
--- a/packages/next/src/client/app-index.tsx
+++ b/packages/next/src/client/app-index.tsx
@@ -66,6 +66,16 @@ const getCacheKey = () => {
return pathname + search
}
+async function sha1(message: string) {
+ const arrayBuffer = await crypto.subtle.digest(
+ 'SHA-1',
+ new TextEncoder().encode(message)
+ )
+ const data = Array.from(new Uint8Array(arrayBuffer))
+ const hex = data.map((b) => b.toString(16).padStart(2, '0')).join('')
+ return hex
+}
+
const encoder = new TextEncoder()
let initialServerDataBuffer: string[] | undefined = undefined
@@ -150,7 +160,32 @@ function useInitialServerResponse(cacheKey: string): Promise {
},
})
- const newResponse = createFromReadableStream(readable)
+ const newResponse = createFromReadableStream(readable, {
+ async callServer(
+ metadata: {
+ id: string
+ name: string
+ },
+ args: any[]
+ ) {
+ const actionId = await sha1(metadata.id + ':' + metadata.name)
+
+ // Fetching the current url with the action header.
+ // TODO: Refactor this to look up from a manifest.
+ const res = await fetch('', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Next-Action': actionId,
+ },
+ body: JSON.stringify({
+ bound: args,
+ }),
+ })
+
+ return res.json()
+ },
+ })
rscCache.set(cacheKey, newResponse)
return newResponse
diff --git a/packages/next/src/client/components/app-router-headers.ts b/packages/next/src/client/components/app-router-headers.ts
index 26886848a0ab8..8fc8a64c3b6f0 100644
--- a/packages/next/src/client/components/app-router-headers.ts
+++ b/packages/next/src/client/components/app-router-headers.ts
@@ -1,5 +1,5 @@
export const RSC = 'RSC' as const
-export const ACTION = 'Action' as const
+export const ACTION = 'Next-Action' as const
export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
diff --git a/packages/next/src/server/app-render.tsx b/packages/next/src/server/app-render.tsx
index b619ccbe428d2..ba8930abe4db5 100644
--- a/packages/next/src/server/app-render.tsx
+++ b/packages/next/src/server/app-render.tsx
@@ -1703,20 +1703,33 @@ export async function renderToHTMLOrFlight(
}
// For action requests, we handle them differently with a sepcial render result.
- if (isAction && process.env.NEXT_RUNTIME !== 'edge') {
- const workerName = 'app' + renderOpts.pathname
- const actionModId = serverActionsManifest[actionId].workers[workerName]
+ if (isAction) {
+ if (process.env.NEXT_RUNTIME !== 'edge') {
+ const workerName = 'app' + renderOpts.pathname
+ const actionModId = serverActionsManifest[actionId].workers[workerName]
- const { parseBody } =
- require('./api-utils/node') as typeof import('./api-utils/node')
- const actionData = (await parseBody(req, '1mb')) || {}
+ const { parseBody } =
+ require('./api-utils/node') as typeof import('./api-utils/node')
+ const actionData = (await parseBody(req, '1mb')) || {}
- const actionHandler =
- ComponentMod.__next_app_webpack_require__(actionModId).default
+ const actionHandler =
+ ComponentMod.__next_app_webpack_require__(actionModId).default
- return new ActionRenderResult(
- JSON.stringify(await actionHandler(actionId, actionData.bound || []))
- )
+ try {
+ return new ActionRenderResult(
+ JSON.stringify(
+ await actionHandler(actionId, actionData.bound || [])
+ )
+ )
+ } catch (err) {
+ if (isRedirectError(err)) {
+ throw new Error('Invariant: not implemented.')
+ }
+ throw err
+ }
+ } else {
+ throw new Error('Not implemented in Edge Runtime.')
+ }
}
// Below this line is handling for rendering to HTML.
diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts
new file mode 100644
index 0000000000000..a4a7e173e5bae
--- /dev/null
+++ b/test/e2e/app-dir/actions/app-action.test.ts
@@ -0,0 +1,38 @@
+import { createNextDescribe } from 'e2e-utils'
+import { check } from 'next-test-utils'
+
+createNextDescribe(
+ 'app-dir action handling',
+ {
+ files: __dirname,
+ skipDeployment: true,
+ },
+ ({ next, isNextDev }) => {
+ if (!isNextDev) {
+ it('should create the server reference manifest', async () => {
+ const content = await next.readFile(
+ '.next/server/server-reference-manifest.json'
+ )
+ // Make sure it's valid JSON
+ JSON.parse(content)
+ expect(content.length > 0).toBeTrue()
+ })
+ }
+
+ // TODO: Ensure this works in production.
+ if (isNextDev) {
+ it('should handle basic actions correctly', async () => {
+ const browser = await next.browser('/server')
+
+ const cnt = await browser.elementByCss('h1').text()
+ expect(cnt).toBe('0')
+
+ await browser.elementByCss('#inc').click()
+ await check(() => browser.elementByCss('h1').text(), '1')
+
+ await browser.elementByCss('#dec').click()
+ await check(() => browser.elementByCss('h1').text(), '0')
+ })
+ }
+ }
+)
diff --git a/test/e2e/app-dir/actions/app/layout.js b/test/e2e/app-dir/actions/app/layout.js
new file mode 100644
index 0000000000000..6d7e1ed585862
--- /dev/null
+++ b/test/e2e/app-dir/actions/app/layout.js
@@ -0,0 +1,8 @@
+export default function RootLayout({ children }) {
+ return (
+
+
+
{children}
+
+ )
+}
diff --git a/test/e2e/app-dir/actions/app/server/actions.js b/test/e2e/app-dir/actions/app/server/actions.js
new file mode 100644
index 0000000000000..fdd9f22f3a658
--- /dev/null
+++ b/test/e2e/app-dir/actions/app/server/actions.js
@@ -0,0 +1,9 @@
+'use server'
+
+export async function inc(value) {
+ return value + 1
+}
+
+export async function dec(value) {
+ return value - 1
+}
diff --git a/test/e2e/app-dir/actions/app/server/counter.js b/test/e2e/app-dir/actions/app/server/counter.js
new file mode 100644
index 0000000000000..b685677ac72ab
--- /dev/null
+++ b/test/e2e/app-dir/actions/app/server/counter.js
@@ -0,0 +1,31 @@
+'use client'
+
+import { useState } from 'react'
+
+export default function Counter({ inc, dec }) {
+ const [count, setCount] = useState(0)
+
+ return (
+
+
{count}
+
+
+
+ )
+}
diff --git a/test/e2e/app-dir/actions/app/server/page.js b/test/e2e/app-dir/actions/app/server/page.js
new file mode 100644
index 0000000000000..7e6f7f73233d9
--- /dev/null
+++ b/test/e2e/app-dir/actions/app/server/page.js
@@ -0,0 +1,7 @@
+import Counter from './counter'
+
+import { inc, dec } from './actions'
+
+export default function Page() {
+ return
+}
diff --git a/test/e2e/app-dir/actions/next.config.js b/test/e2e/app-dir/actions/next.config.js
new file mode 100644
index 0000000000000..cfa3ac3d7aa94
--- /dev/null
+++ b/test/e2e/app-dir/actions/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ appDir: true,
+ },
+}