feat(nextjs): Isolate nonce fetch in Suspense boundary for PPR support#7773
feat(nextjs): Isolate nonce fetch in Suspense boundary for PPR support#7773jacekradko wants to merge 11 commits intomainfrom
Conversation
Move nonce fetching from the server ClerkProvider's main body into a separate DynamicClerkScripts server component wrapped in Suspense. This allows pages using dynamic=true to remain statically renderable and compatible with PPR/cacheComponents. - Create DynamicClerkScripts async server component - Add getNonce cached function to utils - Skip client ClerkScripts when server scripts are used - Pass __internal_skipScripts through KeylessProvider
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
🦋 Changeset detectedLatest commit: 137f9f2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughAdds an internal boolean prop 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
@clerk/agent-toolkit
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
…ndling Extract duplicated script rendering into a shared ClerkScriptTags component used by both ClerkScripts (client) and DynamicClerkScripts (server). Add try/catch to getNonce() so errors in prerendering or "use cache" contexts degrade gracefully instead of propagating unhandled.
…n RSC Import clerkJSScriptUrl, buildClerkJSScriptAttributes, clerkUIScriptUrl from @clerk/shared/loadClerkJsScript instead of @clerk/react/internal in the shared ClerkScriptTags component. The @clerk/react/internal barrel re-exports modules that use React.createContext, which breaks when the RSC bundler evaluates the barrel in server component context.
…t-await-nonce-in-the-clerkprovider
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/nextjs/src/app-router/server/utils.ts`:
- Around line 156-180: Add automated tests for getNonce covering three
scenarios: (1) when the request provides X-Nonce header ensure getNonce returns
that value and caching via reactCache deduplicates repeated calls; (2) when
X-Nonce is absent but Content-Security-Policy contains a script-nonce ensure
getNonce falls back to getScriptNonceFromHeader and returns the extracted nonce;
and (3) when the dynamic import throws a prerendering bailout
(isPrerenderingBailout returns true) ensure the bailout is rethrown. In tests,
mock the dynamic import of 'next/headers' to return a headers() object with
get() behavior, spy/override getScriptNonceFromHeader and isPrerenderingBailout
to simulate CSP and bailout cases, and verify caching semantics by calling
getNonce multiple times in the same test to confirm reactCache deduplication.
| /** | ||
| * Fetches the nonce from request headers. | ||
| * Uses React.cache to deduplicate calls within the same request. | ||
| */ | ||
| // React.cache is only available in RSC environments; provide a no-op fallback for tests/non-RSC contexts. | ||
| const reactCache = typeof React.cache === 'function' ? React.cache : <T extends (...args: any[]) => any>(fn: T): T => fn; | ||
|
|
||
| export const getNonce = reactCache(async function getNonce(): Promise<string> { | ||
| try { | ||
| // Dynamically import next/headers | ||
| // @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307) | ||
| const { headers } = await import('next/headers'); | ||
| const headersList = await headers(); | ||
| const nonce = headersList.get('X-Nonce'); | ||
| return nonce | ||
| ? nonce | ||
| : // Fallback to extracting from CSP header | ||
| getScriptNonceFromHeader(headersList.get('Content-Security-Policy') || '') || ''; | ||
| } catch (e) { | ||
| if (isPrerenderingBailout(e)) { | ||
| throw e; | ||
| } | ||
| // Graceful degradation — scripts load without nonce | ||
| return ''; | ||
| } |
There was a problem hiding this comment.
Add automated coverage for getNonce behavior.
No tests are included for the new nonce retrieval/caching paths; please add tests covering X-Nonce presence, CSP fallback, and prerendering bailout handling.
As per coding guidelines, “If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes.”
🤖 Prompt for AI Agents
In `@packages/nextjs/src/app-router/server/utils.ts` around lines 156 - 180, Add
automated tests for getNonce covering three scenarios: (1) when the request
provides X-Nonce header ensure getNonce returns that value and caching via
reactCache deduplicates repeated calls; (2) when X-Nonce is absent but
Content-Security-Policy contains a script-nonce ensure getNonce falls back to
getScriptNonceFromHeader and returns the extracted nonce; and (3) when the
dynamic import throws a prerendering bailout (isPrerenderingBailout returns
true) ensure the bailout is rethrown. In tests, mock the dynamic import of
'next/headers' to return a headers() object with get() behavior, spy/override
getScriptNonceFromHeader and isPrerenderingBailout to simulate CSP and bailout
cases, and verify caching semantics by calling getNonce multiple times in the
same test to confirm reactCache deduplication.
|
!snapshot |
|
Hey @jacekradko - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/agent-toolkit@0.3.0-snapshot.v20260206212043 --save-exact
npm i @clerk/astro@3.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/backend@3.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/chrome-extension@3.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/clerk-js@6.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/dev-cli@1.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/expo@3.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/express@2.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/fastify@2.7.0-snapshot.v20260206212043 --save-exact
npm i @clerk/localizations@4.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/msw@0.0.1-snapshot.v20260206212043 --save-exact
npm i @clerk/nextjs@7.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/nuxt@2.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/react@6.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/react-router@3.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/shared@4.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/testing@2.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/ui@1.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/upgrade@2.0.0-snapshot.v20260206212043 --save-exact
npm i @clerk/vue@2.0.0-snapshot.v20260206212043 --save-exact |
Summary
DynamicClerkScriptsserver component wrapped in Suspensedynamic=trueto remain statically renderable and compatible with PPR/cacheComponentsProblem
In the Next.js App Router server
ClerkProvider, we awaitnoncefrom headers viagetNonceHeaders(). This callsheaders()which opts the entire page out of static rendering and breaks PPR/cacheComponents.Solution
Isolate the nonce fetch in a Suspense boundary:
DynamicClerkScriptsasync server component that fetches nonce and renders scriptsgetNoncecached function to utilsdynamic=true, render<Suspense><DynamicClerkScripts/></Suspense>ClerkScriptswhen server scripts are used via__internal_skipScriptspropTest plan
dynamic=true- scripts should render correctlyCloses USER-4607
Summary by CodeRabbit
New Features
Refactor