-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(after): allow request APIs in after (actions/handlers) (#73345)
This PR relaxes some of the limitations around calling request APIs (`headers()`, `cookies()`, `connection()`) inside `unstable_after` (which was previously blanket-forbidden since #71231). We're now allowing it in server actions and route handlers[1]: ```tsx // route.ts ✅ export function POST() { unstable_after(async () => console.log(await headers())); } ``` ```tsx // action.ts ✅ export function myAction() { "use server" unstable_after(async () => console.log(await headers())); } ``` however using it in server components remains banned: ```tsx // page.tsx ❌ export default function Page() { unstable_after(async () => console.log(await headers())); } ``` ### Implementation notes The way to detect actions and route handlers is to check if `workUnitStore.phase === "action"` [2]. But we can't use the existing `workUnitStore.phase` for this because it's deliberately mutable, so it'd have changed by the time we get to executing the callbacks and the actual `headers()` call. To solve this, I'm introducing a new ALS store - `afterTaskStore`. It currently only stores the phase that the (topmost) `after` was called in. But this info is also specific to each "task"/callback (or rather, task chain -- nested afters inherit the context of their parents), so we can't use any of the existing stores. I 'm also going to need task-specific info for upcoming devtools work (more useful callstacks, see https://github.com/vercel/next.js/blob/54465dbe782f505fd0105258d2d053a6439457ed/packages/next/src/server/app-render/after-task-async-storage.external.ts#L14), so i think introducing a new store is justified. --- [1] - Note that if it's in a prerendered GET handler (with `dynamic = 'error'`), `headers()` & co. will still error, but that's because they're dynamic, not because they were called within `after`! [2] - `actionAsyncStorage` is a no-go, because `actionStore.isAction` can remain true in the render triggered by an action (or in promises passed into it), which was the reason we introduced `phase` in the first place.
- Loading branch information
1 parent
0edb112
commit 87c2c52
Showing
16 changed files
with
233 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
packages/next/src/server/app-render/after-task-async-storage-instance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { AfterTaskAsyncStorage } from './after-task-async-storage.external' | ||
import { createAsyncLocalStorage } from './async-local-storage' | ||
|
||
export const afterTaskAsyncStorageInstance: AfterTaskAsyncStorage = | ||
createAsyncLocalStorage() |
18 changes: 18 additions & 0 deletions
18
packages/next/src/server/app-render/after-task-async-storage.external.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { AsyncLocalStorage } from 'async_hooks' | ||
|
||
// Share the instance module in the next-shared layer | ||
import { afterTaskAsyncStorageInstance as afterTaskAsyncStorage } from './after-task-async-storage-instance' with { 'turbopack-transition': 'next-shared' } | ||
import type { WorkUnitStore } from './work-unit-async-storage.external' | ||
|
||
export interface AfterTaskStore { | ||
/** The phase in which the topmost `unstable_after` was called. | ||
* | ||
* NOTE: Can be undefined when running `generateStaticParams`, | ||
* where we only have a `workStore`, no `workUnitStore`. | ||
*/ | ||
readonly rootTaskSpawnPhase: WorkUnitStore['phase'] | undefined | ||
} | ||
|
||
export type AfterTaskAsyncStorage = AsyncLocalStorage<AfterTaskStore> | ||
|
||
export { afterTaskAsyncStorage } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 40 additions & 7 deletions
47
test/e2e/app-dir/next-after-app-api-usage/app/request-apis/helpers.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,64 @@ | ||
import { cookies, headers } from 'next/headers' | ||
import { unstable_after as after, connection } from 'next/server' | ||
|
||
export function testRequestAPIs() { | ||
export function testRequestAPIs(/** @type {string} */ route) { | ||
after(async () => { | ||
try { | ||
await headers() | ||
console.log('headers(): ok') | ||
console.log(`[${route}] headers(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] headers(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await headers() | ||
console.log(`[${route}] nested headers(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested headers(): error:`, err) | ||
} | ||
}) | ||
) | ||
|
||
after(async () => { | ||
try { | ||
await cookies() | ||
console.log('cookies(): ok') | ||
console.log(`[${route}] cookies(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] cookies(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await cookies() | ||
console.log(`[${route}] nested cookies(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested cookies(): error:`, err) | ||
} | ||
}) | ||
) | ||
|
||
after(async () => { | ||
try { | ||
await connection() | ||
console.log('connection(): ok') | ||
console.log(`[${route}] connection(): ok`) | ||
} catch (err) { | ||
console.error(err) | ||
console.error(`[${route}] connection(): error:`, err) | ||
} | ||
}) | ||
|
||
after(() => | ||
after(async () => { | ||
try { | ||
await connection() | ||
console.log(`[${route}] nested connection(): ok`) | ||
} catch (err) { | ||
console.error(`[${route}] nested connection(): error:`, err) | ||
} | ||
}) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
test/e2e/app-dir/next-after-app-api-usage/app/request-apis/route-handler-dynamic/route.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { testRequestAPIs } from '../helpers' | ||
|
||
export async function GET() { | ||
testRequestAPIs() | ||
testRequestAPIs('/request-apis/route-handler-dynamic') | ||
return new Response() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.