Skip to content

Commit

Permalink
Do not invoke server actions twice when dynamicIO is enabled
Browse files Browse the repository at this point in the history
In dev mode, when dynamic IO is enabled, we seed a prefetch cache scope
for non-prefetch requests (via #70408).

We must omit server action requests from this cache seeding though,
because it would lead to the action handler being called twice. This is
not intended in general for any action, and specificially fails in the
second invocation while decoding the request form data with `Error:
Unexpected end of JSON input` in `initializeModelChunk`, because the
request body was already consumed in the first invocation.
  • Loading branch information
unstubbable committed Oct 4, 2024
1 parent d5fd4c1 commit 570aaf4
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3038,7 +3038,8 @@ export default abstract class Server<
if (
!cache &&
!isPrefetchRSCRequest &&
routeModule?.definition.kind === RouteKind.APP_PAGE
routeModule?.definition.kind === RouteKind.APP_PAGE &&
!isServerAction
) {
req.headers[RSC_HEADER] = '1'
req.headers[NEXT_ROUTER_PREFETCH_HEADER] = '1'
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/app-dir/dynamic-io-server-action/app/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use server'

export async function action(value: string) {
return value
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/dynamic-io-server-action/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
16 changes: 16 additions & 0 deletions test/e2e/app-dir/dynamic-io-server-action/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'

import { useActionState } from 'react'
import { action } from './action'

export default function Page() {
const [result, formAction] = useActionState(() => action('result'), 'initial')

return (
<form action={formAction}>
<h1>Server Action with Dynamic IO</h1>
<button>Submit</button>
<p>{result}</p>
</form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'

describe('dynamic-io-server-action', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('should not fail decoding server action arguments', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('initial')
await browser.elementByCss('button').click()

await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('result')
})
})
})
10 changes: 10 additions & 0 deletions test/e2e/app-dir/dynamic-io-server-action/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
dynamicIO: true,
},
}

module.exports = nextConfig

0 comments on commit 570aaf4

Please sign in to comment.