Skip to content

Commit 887d746

Browse files
authored
Fix accessing headers in progressively enhanced form actions (vercel#74196)
When scoping the `requestStore` to dynamic renders in vercel#72312, we missed the case when a server action is invoked before hydration is complete. This leads to an error when a server action tries to read headers, cookies, or anything else that requires the presence of the `requestStore`. This fixes that for both the Node.js and Edge runtimes. It also appears that progressively enhanced form actions did not work at all before in the Edge runtime due to a missing `await` of `decodeFormState`. fixes vercel#73992 closes NAR-55
1 parent 2e46a18 commit 887d746

File tree

4 files changed

+97
-15
lines changed

4 files changed

+97
-15
lines changed

packages/next/src/server/app-render/action-handler.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,12 +657,19 @@ export async function handleAction({
657657
if (typeof action === 'function') {
658658
// Only warn if it's a server action, otherwise skip for other post requests
659659
warnBadServerActionRequest()
660-
const actionReturnedState = await action()
661-
formState = decodeFormState(
660+
661+
const actionReturnedState = await workUnitAsyncStorage.run(
662+
requestStore,
663+
action
664+
)
665+
666+
formState = await decodeFormState(
662667
actionReturnedState,
663668
formData,
664669
serverModuleMap
665670
)
671+
672+
requestStore.phase = 'render'
666673
}
667674

668675
// Skip the fetch path
@@ -804,12 +811,19 @@ export async function handleAction({
804811
if (typeof action === 'function') {
805812
// Only warn if it's a server action, otherwise skip for other post requests
806813
warnBadServerActionRequest()
807-
const actionReturnedState = await action()
814+
815+
const actionReturnedState = await workUnitAsyncStorage.run(
816+
requestStore,
817+
action
818+
)
819+
808820
formState = await decodeFormState(
809821
actionReturnedState,
810822
formData,
811823
serverModuleMap
812824
)
825+
826+
requestStore.phase = 'render'
813827
}
814828

815829
// Skip the fetch path

test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable jest/no-standalone-expect */
22
import { nextTestSetup } from 'e2e-utils'
3-
import { check } from 'next-test-utils'
3+
import { retry } from 'next-test-utils'
44
import type { Response } from 'playwright'
55

66
describe('app-dir action progressive enhancement', () => {
@@ -13,7 +13,7 @@ describe('app-dir action progressive enhancement', () => {
1313
})
1414

1515
it('should support formData and redirect without JS', async () => {
16-
let responseCode
16+
let responseCode: number | undefined
1717
const browser = await next.browser('/server', {
1818
disableJavaScript: true,
1919
beforePageLoad(page) {
@@ -27,12 +27,14 @@ describe('app-dir action progressive enhancement', () => {
2727
},
2828
})
2929

30-
await browser.eval(`document.getElementById('name').value = 'test'`)
31-
await browser.elementByCss('#submit').click()
30+
await browser.elementById('name').type('test')
31+
await browser.elementById('submit').click()
3232

33-
await check(() => {
34-
return browser.eval('window.location.pathname + window.location.search')
35-
}, '/header?name=test&hidden-info=hi')
33+
await retry(async () => {
34+
expect(await browser.url()).toBe(
35+
`${next.url}/header?name=test&hidden-info=hi`
36+
)
37+
})
3638

3739
expect(responseCode).toBe(303)
3840
})
@@ -42,11 +44,42 @@ describe('app-dir action progressive enhancement', () => {
4244
disableJavaScript: true,
4345
})
4446

45-
await browser.eval(`document.getElementById('client-name').value = 'test'`)
46-
await browser.elementByCss('#there').click()
47+
await browser.elementById('client-name').type('test')
48+
await browser.elementById('there').click()
4749

48-
await check(() => {
49-
return browser.eval('window.location.pathname + window.location.search')
50-
}, '/header?name=test&hidden-info=hi')
50+
await retry(async () => {
51+
expect(await browser.url()).toBe(
52+
`${next.url}/header?name=test&hidden-info=hi`
53+
)
54+
})
5155
})
56+
57+
it.each(['edge', 'node'])(
58+
'should support headers and cookies without JS (runtime: %s)',
59+
async (runtime) => {
60+
const browser = await next.browser(`/header/${runtime}/form`, {
61+
disableJavaScript: true,
62+
})
63+
64+
await browser.elementById('get-referer').click()
65+
66+
await retry(async () => {
67+
expect(await browser.elementById('referer').text()).toBe(
68+
`${next.url}/header/${runtime}/form`
69+
)
70+
})
71+
72+
await browser.elementById('set-cookie').click()
73+
74+
await retry(async () => {
75+
expect(await browser.elementById('referer').text()).toBe('')
76+
})
77+
78+
await browser.elementById('get-cookie').click()
79+
80+
await retry(async () => {
81+
expect(await browser.elementById('cookie').text()).toBe('42')
82+
})
83+
}
84+
)
5285
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from '../../node/form/page'
2+
3+
export const runtime = 'edge'
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client'
2+
3+
import { useActionState } from 'react'
4+
import { getCookie, getHeader, setCookie } from '../../actions'
5+
6+
const getReferrer = getHeader.bind(null, 'referer')
7+
const setTestCookie = setCookie.bind(null, 'test', '42')
8+
const getTestCookie = getCookie.bind(null, 'test')
9+
10+
export default function Page() {
11+
const [referer, getReferrerAction] = useActionState(getReferrer, null)
12+
13+
const [cookie, getTestCookieAction] = useActionState<ReturnType<
14+
typeof getCookie
15+
> | null>(getTestCookie, null)
16+
17+
return (
18+
<>
19+
<form action={getReferrerAction}>
20+
<p id="referer">{referer}</p>
21+
<button id="get-referer">Get Referer</button>
22+
</form>
23+
<form action={getTestCookieAction}>
24+
<p id="cookie">{cookie?.value}</p>
25+
<button id="set-cookie" formAction={setTestCookie}>
26+
Set Cookie
27+
</button>
28+
<button id="get-cookie">Get Cookie</button>
29+
</form>
30+
</>
31+
)
32+
}

0 commit comments

Comments
 (0)