From 3ce60cf19edc8f0f04a1d1a82a695f5a9636856b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 1 Jan 2025 15:26:24 +0200 Subject: [PATCH 1/7] fix: Handle `dynamicIO` errors when request apis are accessed on prerender --- .../nextjs/src/app-router/server/utils.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index 7e8c220155f..73c47ec21db 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -19,7 +19,21 @@ export const isPrerenderingBailout = (e: unknown) => { return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering; }; -export async function buildRequestLike() { +/** + * Detects dynamic APIs when dynamicIO is enabled (Canary only). + * https://github.com/vercel/next.js/blob/35acd7e1faae66feddeffe6362fae9fb5a5b1281/packages/next/src/server/dynamic-rendering-utils.ts#L18 + */ +export const isDynamicIOPrerenderingBailout = (e: unknown) => { + if (!(e instanceof Error) || !('message' in e)) { + return false; + } + + const { message } = e; + const routeRegex = /^During prerendering, .* rejects when the prerender is complete/; + return routeRegex.test(message); +}; + +export async function buildRequestLike(): Promise { try { // Dynamically import next/headers, otherwise Next12 apps will break // @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307) @@ -27,6 +41,11 @@ export async function buildRequestLike() { const resolvedHeaders = await headers(); return new NextRequest('https://placeholder.com', { headers: resolvedHeaders }); } catch (e: any) { + // While generating the static shell usage of `headers()` will throw. We can gracefully return an empty request. + if (e && isDynamicIOPrerenderingBailout(e)) { + return new NextRequest('https://placeholder.com'); + } + // rethrow the error when react throws a prerendering bailout // https://nextjs.org/docs/messages/ppr-caught-error if (e && isPrerenderingBailout(e)) { From ae7cdb0c8da3e14861dbb960b173b62eb3f153be Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 1 Jan 2025 17:31:30 +0200 Subject: [PATCH 2/7] first solution --- packages/nextjs/src/app-router/server/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index 73c47ec21db..067180db32c 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -43,7 +43,9 @@ export async function buildRequestLike(): Promise { } catch (e: any) { // While generating the static shell usage of `headers()` will throw. We can gracefully return an empty request. if (e && isDynamicIOPrerenderingBailout(e)) { - return new NextRequest('https://placeholder.com'); + return new NextRequest('https://placeholder.com', { + headers: new Headers({ 'x-clerk-auth-status': 'signed-out' }), + }); } // rethrow the error when react throws a prerendering bailout From 6c8f074b267cf7c69e8c25605d59858f1bd9dbce Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 1 Jan 2025 17:32:58 +0200 Subject: [PATCH 3/7] wip --- .../src/app-router/server/ClerkProvider.tsx | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 69f3693a568..1c55e30a269 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,9 +1,7 @@ import type { AuthObject } from '@clerk/backend'; -import type { InitialState, Without } from '@clerk/types'; -import { headers } from 'next/headers'; +import type { Without } from '@clerk/types'; import React from 'react'; -import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider'; import { getDynamicAuthData } from '../../server/buildClerkProps'; import type { NextClerkProviderProps } from '../../types'; import { canUseKeyless__server } from '../../utils/feature-flags'; @@ -12,23 +10,47 @@ import { isNext13 } from '../../utils/sdk-versions'; import { ClientClerkProvider } from '../client/ClerkProvider'; import { buildRequestLike, getScriptNonceFromHeader } from './utils'; -const getDynamicClerkState = React.cache(async function getDynamicClerkState() { +// const getNonceFromCSPHeader = async function getNonceFromCSPHeader() { +// // const request = +// // await buildRequestLike(); +// // return ''; +// const request = await buildRequestLike(); +// // const data = +// getDynamicAuthData(request); +// +// return ''; +// // try { +// // return getScriptNonceFromHeader(request.headers.get('Content-Security-Policy') || '') || ''; +// // } catch (e) { +// // console.log('Failed from getNonceFromCSPHeader', e); +// // } +// // return ''; +// }; + +const getDynamicClerkState = async function getDynamicClerkState() { + // try { const request = await buildRequestLike(); const data = getDynamicAuthData(request); - return data; -}); + return [data, getScriptNonceFromHeader(request.headers.get('Content-Security-Policy') || '') || ''] as [ + AuthObject, + string, + ]; + // } catch (e) { + // console.log('Failed getDynamicClerkState', e); + // } + // return null; +}; +getDynamicClerkState; -const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader() { - return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; -}); +// getNonceFromCSPHeader; export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let statePromise: Promise = Promise.resolve(null); - let nonce = Promise.resolve(''); + let statePromise: Promise<[null | AuthObject, string]> = Promise.resolve([null, '']); + // const nonce = Promise.resolve(''); if (dynamic) { if (isNext13) { @@ -36,11 +58,11 @@ export async function ClerkProvider( * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. * Without the await here, Next will throw a DynamicServerError during build. */ - statePromise = Promise.resolve(await getDynamicClerkState()); - nonce = Promise.resolve(await getNonceFromCSPHeader()); + // statePromise = Promise.resolve(await getDynamicClerkState()); + // nonce = Promise.resolve(await getNonceFromCSPHeader()); } else { statePromise = getDynamicClerkState(); - nonce = getNonceFromCSPHeader(); + // nonce = getNonceFromCSPHeader(); } } @@ -51,8 +73,8 @@ export async function ClerkProvider( let output = ( {children} @@ -75,8 +97,8 @@ export async function ClerkProvider( __internal_claimKeylessApplicationUrl: newOrReadKeys.claimUrl, __internal_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl, })} - nonce={await nonce} - initialState={await statePromise} + initialState={(await statePromise)[0]} + nonce={(await statePromise)[1]} > {children} @@ -85,14 +107,14 @@ export async function ClerkProvider( } } - if (dynamic) { - return ( - // TODO: fix types so AuthObject is compatible with InitialState - }> - {output} - - ); - } + // if (dynamic) { + // return ( + // // TODO: fix types so AuthObject is compatible with InitialState + // }> + // {output} + // + // ); + // } return output; } From 3ed5874ce9dc310eb95bd5e47fc5c15340606eca Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 1 Jan 2025 17:33:16 +0200 Subject: [PATCH 4/7] Revert "wip" This reverts commit 6c8f074b267cf7c69e8c25605d59858f1bd9dbce. --- .../src/app-router/server/ClerkProvider.tsx | 74 +++++++------------ 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 1c55e30a269..69f3693a568 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,7 +1,9 @@ import type { AuthObject } from '@clerk/backend'; -import type { Without } from '@clerk/types'; +import type { InitialState, Without } from '@clerk/types'; +import { headers } from 'next/headers'; import React from 'react'; +import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider'; import { getDynamicAuthData } from '../../server/buildClerkProps'; import type { NextClerkProviderProps } from '../../types'; import { canUseKeyless__server } from '../../utils/feature-flags'; @@ -10,47 +12,23 @@ import { isNext13 } from '../../utils/sdk-versions'; import { ClientClerkProvider } from '../client/ClerkProvider'; import { buildRequestLike, getScriptNonceFromHeader } from './utils'; -// const getNonceFromCSPHeader = async function getNonceFromCSPHeader() { -// // const request = -// // await buildRequestLike(); -// // return ''; -// const request = await buildRequestLike(); -// // const data = -// getDynamicAuthData(request); -// -// return ''; -// // try { -// // return getScriptNonceFromHeader(request.headers.get('Content-Security-Policy') || '') || ''; -// // } catch (e) { -// // console.log('Failed from getNonceFromCSPHeader', e); -// // } -// // return ''; -// }; - -const getDynamicClerkState = async function getDynamicClerkState() { - // try { +const getDynamicClerkState = React.cache(async function getDynamicClerkState() { const request = await buildRequestLike(); const data = getDynamicAuthData(request); - return [data, getScriptNonceFromHeader(request.headers.get('Content-Security-Policy') || '') || ''] as [ - AuthObject, - string, - ]; - // } catch (e) { - // console.log('Failed getDynamicClerkState', e); - // } - // return null; -}; -getDynamicClerkState; + return data; +}); -// getNonceFromCSPHeader; +const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader() { + return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; +}); export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let statePromise: Promise<[null | AuthObject, string]> = Promise.resolve([null, '']); - // const nonce = Promise.resolve(''); + let statePromise: Promise = Promise.resolve(null); + let nonce = Promise.resolve(''); if (dynamic) { if (isNext13) { @@ -58,11 +36,11 @@ export async function ClerkProvider( * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. * Without the await here, Next will throw a DynamicServerError during build. */ - // statePromise = Promise.resolve(await getDynamicClerkState()); - // nonce = Promise.resolve(await getNonceFromCSPHeader()); + statePromise = Promise.resolve(await getDynamicClerkState()); + nonce = Promise.resolve(await getNonceFromCSPHeader()); } else { statePromise = getDynamicClerkState(); - // nonce = getNonceFromCSPHeader(); + nonce = getNonceFromCSPHeader(); } } @@ -73,8 +51,8 @@ export async function ClerkProvider( let output = ( {children} @@ -97,8 +75,8 @@ export async function ClerkProvider( __internal_claimKeylessApplicationUrl: newOrReadKeys.claimUrl, __internal_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl, })} - initialState={(await statePromise)[0]} - nonce={(await statePromise)[1]} + nonce={await nonce} + initialState={await statePromise} > {children} @@ -107,14 +85,14 @@ export async function ClerkProvider( } } - // if (dynamic) { - // return ( - // // TODO: fix types so AuthObject is compatible with InitialState - // }> - // {output} - // - // ); - // } + if (dynamic) { + return ( + // TODO: fix types so AuthObject is compatible with InitialState + }> + {output} + + ); + } return output; } From 01fd207f2b156531d5a125b5c46b8078620a24aa Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 1 Jan 2025 17:42:02 +0200 Subject: [PATCH 5/7] add changeset --- .changeset/brown-kids-camp.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brown-kids-camp.md diff --git a/.changeset/brown-kids-camp.md b/.changeset/brown-kids-camp.md new file mode 100644 index 00000000000..8b8248b3152 --- /dev/null +++ b/.changeset/brown-kids-camp.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Handle `dynamicIO` errors when request apis are accessed on prerender. This fixes issues with `ppr: true, dynamicIO: true` when using ``. From d90938f72b22c3da7537cf38146e55b88bd1bb8d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 9 Jan 2025 23:02:58 +0200 Subject: [PATCH 6/7] fix(nextjs): restructure promise creation in server ClerkProvider --- .../src/app-router/server/ClerkProvider.tsx | 51 ++++++++++++------- .../nextjs/src/app-router/server/utils.ts | 23 +-------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 69f3693a568..2646f98bba0 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,4 +1,3 @@ -import type { AuthObject } from '@clerk/backend'; import type { InitialState, Without } from '@clerk/types'; import { headers } from 'next/headers'; import React from 'react'; @@ -27,21 +26,35 @@ export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let statePromise: Promise = Promise.resolve(null); - let nonce = Promise.resolve(''); - if (dynamic) { - if (isNext13) { - /** - * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. - * Without the await here, Next will throw a DynamicServerError during build. - */ - statePromise = Promise.resolve(await getDynamicClerkState()); - nonce = Promise.resolve(await getNonceFromCSPHeader()); - } else { - statePromise = getDynamicClerkState(); - nonce = getNonceFromCSPHeader(); + async function generateStatePromise() { + if (dynamic) { + if (isNext13) { + /** + * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. + * Without the await here, Next will throw a DynamicServerError during build. + */ + return Promise.resolve(await getDynamicClerkState()); + } else { + return getDynamicClerkState(); + } + } + return Promise.resolve(null); + } + + async function generateNonce() { + if (dynamic) { + if (isNext13) { + /** + * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. + * Without the await here, Next will throw a DynamicServerError during build. + */ + return Promise.resolve(await getNonceFromCSPHeader()); + } else { + return getNonceFromCSPHeader(); + } } + return Promise.resolve(''); } const propsWithEnvs = mergeNextClerkPropsWithEnv({ @@ -51,8 +64,8 @@ export async function ClerkProvider( let output = ( {children} @@ -75,8 +88,8 @@ export async function ClerkProvider( __internal_claimKeylessApplicationUrl: newOrReadKeys.claimUrl, __internal_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl, })} - nonce={await nonce} - initialState={await statePromise} + nonce={await generateNonce()} + initialState={await generateStatePromise()} > {children} @@ -88,7 +101,7 @@ export async function ClerkProvider( if (dynamic) { return ( // TODO: fix types so AuthObject is compatible with InitialState - }> + }> {output} ); diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index 067180db32c..490166d3027 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -19,20 +19,6 @@ export const isPrerenderingBailout = (e: unknown) => { return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering; }; -/** - * Detects dynamic APIs when dynamicIO is enabled (Canary only). - * https://github.com/vercel/next.js/blob/35acd7e1faae66feddeffe6362fae9fb5a5b1281/packages/next/src/server/dynamic-rendering-utils.ts#L18 - */ -export const isDynamicIOPrerenderingBailout = (e: unknown) => { - if (!(e instanceof Error) || !('message' in e)) { - return false; - } - - const { message } = e; - const routeRegex = /^During prerendering, .* rejects when the prerender is complete/; - return routeRegex.test(message); -}; - export async function buildRequestLike(): Promise { try { // Dynamically import next/headers, otherwise Next12 apps will break @@ -41,13 +27,6 @@ export async function buildRequestLike(): Promise { const resolvedHeaders = await headers(); return new NextRequest('https://placeholder.com', { headers: resolvedHeaders }); } catch (e: any) { - // While generating the static shell usage of `headers()` will throw. We can gracefully return an empty request. - if (e && isDynamicIOPrerenderingBailout(e)) { - return new NextRequest('https://placeholder.com', { - headers: new Headers({ 'x-clerk-auth-status': 'signed-out' }), - }); - } - // rethrow the error when react throws a prerendering bailout // https://nextjs.org/docs/messages/ppr-caught-error if (e && isPrerenderingBailout(e)) { @@ -88,7 +67,7 @@ export function getScriptNonceFromHeader(cspHeaderValue: string): string | undef // Grab the nonce by trimming the 'nonce-' prefix. ?.slice(7, -1); - // If we could't find the nonce, then we're done. + // If we couldn't find the nonce, then we're done. if (!nonce) { return; } From 7007c06be4032254aa4419bdae0ff1a517566ef3 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 13 Jan 2025 11:17:35 +0200 Subject: [PATCH 7/7] improve readability --- .../src/app-router/server/ClerkProvider.tsx | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 2646f98bba0..badcbd19519 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -28,33 +28,31 @@ export async function ClerkProvider( const { children, dynamic, ...rest } = props; async function generateStatePromise() { - if (dynamic) { - if (isNext13) { - /** - * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. - * Without the await here, Next will throw a DynamicServerError during build. - */ - return Promise.resolve(await getDynamicClerkState()); - } else { - return getDynamicClerkState(); - } + if (!dynamic) { + return Promise.resolve(null); } - return Promise.resolve(null); + if (isNext13) { + /** + * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. + * Without the await here, Next will throw a DynamicServerError during build. + */ + return Promise.resolve(await getDynamicClerkState()); + } + return getDynamicClerkState(); } async function generateNonce() { - if (dynamic) { - if (isNext13) { - /** - * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. - * Without the await here, Next will throw a DynamicServerError during build. - */ - return Promise.resolve(await getNonceFromCSPHeader()); - } else { - return getNonceFromCSPHeader(); - } + if (!dynamic) { + return Promise.resolve(''); + } + if (isNext13) { + /** + * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. + * Without the await here, Next will throw a DynamicServerError during build. + */ + return Promise.resolve(await getNonceFromCSPHeader()); } - return Promise.resolve(''); + return getNonceFromCSPHeader(); } const propsWithEnvs = mergeNextClerkPropsWithEnv({