Files
next.js/test/production/standalone-mode/server-actions/standalone-mode-server-actions.test.ts
Hendrik LiebauandGitHub 8cbc03772b Fix server actions in standalone mode with cacheComponents (#91711)
The `staticPathKey` condition added in #91231 inadvertently applies to
server action requests on dynamic SSG routes when `cacheComponents` is
enabled.

Server action fetch requests from the client do not send the `RSC`
header (`rsc: 1`). They only send `Accept: text/x-component` and the
`Next-Action` header. This means `isRSCRequest` and
`isDynamicRSCRequest` are both `false` for action requests. The new
`staticPathKey` condition (`isSSG && pageIsDynamic &&
prerenderInfo?.fallbackRouteParams`) evaluates to `true` for dynamic PPR
routes, setting `staticPathKey` even though `ssgCacheKey` is `null` for
actions.

With `staticPathKey` set, the request enters the fallback rendering
block, which serves the cached fallback HTML shell with the action
result appended to it, instead of responding with just the RSC action
result.

The fix excludes server action requests from the `staticPathKey`
computation by adding `!isPossibleServerAction` to the condition,
restoring the pre-#91231 behavior where `staticPathKey` was always
`null` for server actions in production.

fixes #91662
closes #91677
closes #91669
2026-03-20 14:38:28 +01:00

77 lines
1.9 KiB
TypeScript

import { ChildProcess } from 'child_process'
import { NextInstance, createNext } from 'e2e-utils'
import fs from 'fs-extra'
import { findPort, initNextServerScript, killApp } from 'next-test-utils'
import { join } from 'path'
import webdriver from 'next-webdriver'
describe('standalone mode: server actions', () => {
let next: NextInstance
let server: ChildProcess
let appPort: number
beforeAll(async () => {
next = await createNext({
files: __dirname,
skipStart: true,
})
await next.build()
await fs.move(
join(next.testDir, '.next/standalone'),
join(next.testDir, 'standalone')
)
await fs.copy(
join(next.testDir, 'public'),
join(next.testDir, 'standalone/public')
)
await fs.copy(
join(next.testDir, '.next/static'),
join(next.testDir, 'standalone/.next/static')
)
for (const file of await fs.readdir(next.testDir)) {
if (file !== 'standalone') {
await fs.remove(join(next.testDir, file))
console.log('removed', file)
}
}
const testServer = join(next.testDir, 'standalone/server.js')
appPort = await findPort()
server = await initNextServerScript(testServer, /- Local:/, {
...process.env,
...next.env,
HOSTNAME: '::',
PORT: '' + appPort,
})
})
afterAll(async () => {
await next.destroy()
if (server) {
await killApp(server)
}
})
it('should be able to execute server actions', async () => {
const browser = await webdriver(appPort, `/world`)
await browser.elementByCss('button').click()
expect(await browser.elementByCss('#result').text()).toBe('hello world')
})
it('should be able to execute MPA server actions', async () => {
const browser = await webdriver(appPort, `/world`, {
disableJavaScript: true,
})
await browser.elementByCss('button').click()
expect(await browser.elementByCss('#result').text()).toBe('hello world')
})
})