Skip to content

Commit 3d14a18

Browse files
committed
fix(nextjs): Extract shared ClerkScriptTags and add getNonce error handling
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.
1 parent 141ec6e commit 3d14a18

4 files changed

Lines changed: 108 additions & 68 deletions

File tree

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { buildClerkJSScriptAttributes, clerkJSScriptUrl, clerkUIScriptUrl } from '@clerk/react/internal';
21
import React from 'react';
32

3+
import { ClerkScriptTags } from '../../utils/clerk-script-tags';
44
import { getNonce } from './utils';
55

66
type DynamicClerkScriptsProps = {
@@ -27,43 +27,16 @@ export async function DynamicClerkScripts(props: DynamicClerkScriptsProps) {
2727

2828
const nonce = await getNonce();
2929

30-
const opts = {
31-
publishableKey,
32-
clerkJSUrl,
33-
clerkJSVersion,
34-
clerkUIUrl,
35-
nonce,
36-
domain,
37-
proxyUrl,
38-
};
39-
40-
const scriptUrl = clerkJSScriptUrl(opts);
41-
const attributes = buildClerkJSScriptAttributes(opts);
42-
4330
return (
44-
<>
45-
<script
46-
src={scriptUrl}
47-
data-clerk-js-script
48-
async
49-
crossOrigin='anonymous'
50-
{...attributes}
51-
/>
52-
{/* Use <link rel='preload'> instead of <script> for the UI bundle.
53-
This tells the browser to download the resource immediately (high priority)
54-
but doesn't execute it, avoiding race conditions with __clerkSharedModules
55-
registration (which happens when React code runs @clerk/ui/register).
56-
When loadClerkUIScript() later adds a <script> tag, the browser uses the
57-
cached resource and executes it without re-downloading. */}
58-
{prefetchUI !== false && (
59-
<link
60-
rel='preload'
61-
href={clerkUIScriptUrl(opts)}
62-
as='script'
63-
crossOrigin='anonymous'
64-
nonce={nonce}
65-
/>
66-
)}
67-
</>
31+
<ClerkScriptTags
32+
publishableKey={publishableKey}
33+
clerkJSUrl={clerkJSUrl}
34+
clerkJSVersion={clerkJSVersion}
35+
clerkUIUrl={clerkUIUrl}
36+
nonce={nonce}
37+
domain={domain}
38+
proxyUrl={proxyUrl}
39+
prefetchUI={prefetchUI}
40+
/>
6841
);
6942
}

packages/nextjs/src/app-router/server/utils.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,21 @@ export function getScriptNonceFromHeader(cspHeaderValue: string): string | undef
158158
* Uses React.cache to deduplicate calls within the same request.
159159
*/
160160
export const getNonce = React.cache(async function getNonce(): Promise<string> {
161-
// Dynamically import next/headers
162-
// @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307)
163-
const { headers } = await import('next/headers');
164-
const headersList = await headers();
165-
const nonce = headersList.get('X-Nonce');
166-
return nonce
167-
? nonce
168-
: // Fallback to extracting from CSP header
169-
getScriptNonceFromHeader(headersList.get('Content-Security-Policy') || '') || '';
161+
try {
162+
// Dynamically import next/headers
163+
// @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307)
164+
const { headers } = await import('next/headers');
165+
const headersList = await headers();
166+
const nonce = headersList.get('X-Nonce');
167+
return nonce
168+
? nonce
169+
: // Fallback to extracting from CSP header
170+
getScriptNonceFromHeader(headersList.get('Content-Security-Policy') || '') || '';
171+
} catch (e) {
172+
if (isPrerenderingBailout(e)) {
173+
throw e;
174+
}
175+
// Graceful degradation — scripts load without nonce
176+
return '';
177+
}
170178
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { buildClerkJSScriptAttributes, clerkJSScriptUrl, clerkUIScriptUrl } from '@clerk/react/internal';
2+
import React from 'react';
3+
4+
type ClerkScriptTagsProps = {
5+
publishableKey: string;
6+
clerkJSUrl?: string;
7+
clerkJSVersion?: string;
8+
clerkUIUrl?: string;
9+
nonce?: string;
10+
domain?: string;
11+
proxyUrl?: string;
12+
prefetchUI?: boolean;
13+
};
14+
15+
/**
16+
* Pure component that renders the Clerk script tags.
17+
* Shared between `ClerkScripts` (client, app router) and `DynamicClerkScripts` (server).
18+
* No hooks or client-only imports — safe for both server and client components.
19+
*/
20+
export function ClerkScriptTags(props: ClerkScriptTagsProps) {
21+
const { publishableKey, clerkJSUrl, clerkJSVersion, clerkUIUrl, nonce, domain, proxyUrl, prefetchUI } = props;
22+
23+
const opts = {
24+
publishableKey,
25+
clerkJSUrl,
26+
clerkJSVersion,
27+
clerkUIUrl,
28+
nonce,
29+
domain,
30+
proxyUrl,
31+
};
32+
33+
return (
34+
<>
35+
<script
36+
src={clerkJSScriptUrl(opts)}
37+
data-clerk-js-script
38+
async
39+
crossOrigin='anonymous'
40+
{...buildClerkJSScriptAttributes(opts)}
41+
/>
42+
{/* Use <link rel='preload'> instead of <script> for the UI bundle.
43+
This tells the browser to download the resource immediately (high priority)
44+
but doesn't execute it, avoiding race conditions with __clerkSharedModules
45+
registration (which happens when React code runs @clerk/ui/register).
46+
When loadClerkUIScript() later adds a <script> tag, the browser uses the
47+
cached resource and executes it without re-downloading. */}
48+
{prefetchUI !== false && (
49+
<link
50+
rel='preload'
51+
href={clerkUIScriptUrl(opts)}
52+
as='script'
53+
crossOrigin='anonymous'
54+
nonce={nonce}
55+
/>
56+
)}
57+
</>
58+
);
59+
}

packages/nextjs/src/utils/clerk-script.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,54 @@ import NextScript from 'next/script';
44
import React from 'react';
55

66
import { useClerkNextOptions } from '../client-boundary/NextOptionsContext';
7+
import { ClerkScriptTags } from './clerk-script-tags';
78

89
type ClerkScriptProps = {
910
scriptUrl: string;
1011
attributes: Record<string, string>;
1112
dataAttribute: string;
12-
router: 'app' | 'pages';
1313
};
1414

1515
function ClerkScript(props: ClerkScriptProps) {
16-
const { scriptUrl, attributes, dataAttribute, router } = props;
17-
18-
/**
19-
* Notes:
20-
* `next/script` in 13.x.x when used with App Router will fail to pass any of our `data-*` attributes, resulting in errors
21-
* Nextjs App Router will automatically move inline scripts inside `<head/>`
22-
* Using the `nextjs/script` for App Router with the `beforeInteractive` strategy will throw an error because our custom script will be mounted outside the `html` tag.
23-
*/
24-
const Script = router === 'app' ? 'script' : NextScript;
16+
const { scriptUrl, attributes, dataAttribute } = props;
2517

2618
return (
27-
<Script
19+
<NextScript
2820
src={scriptUrl}
2921
{...{ [dataAttribute]: true }}
3022
async
3123
// `nextjs/script` will add defer by default and does not get removed when async is true
32-
defer={router === 'pages' ? false : undefined}
24+
defer={false}
3325
crossOrigin='anonymous'
34-
strategy={router === 'pages' ? 'beforeInteractive' : undefined}
26+
strategy='beforeInteractive'
3527
{...attributes}
3628
/>
3729
);
3830
}
3931

40-
export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] }) {
32+
export function ClerkScripts({ router }: { router: 'app' | 'pages' }) {
4133
const { publishableKey, clerkJSUrl, clerkJSVersion, clerkUIUrl, nonce, prefetchUI } = useClerkNextOptions();
4234
const { domain, proxyUrl } = useClerk();
4335

4436
if (!publishableKey) {
4537
return null;
4638
}
4739

40+
if (router === 'app') {
41+
return (
42+
<ClerkScriptTags
43+
publishableKey={publishableKey}
44+
clerkJSUrl={clerkJSUrl}
45+
clerkJSVersion={clerkJSVersion}
46+
clerkUIUrl={clerkUIUrl}
47+
nonce={nonce}
48+
domain={domain}
49+
proxyUrl={proxyUrl}
50+
prefetchUI={prefetchUI}
51+
/>
52+
);
53+
}
54+
4855
const opts = {
4956
publishableKey,
5057
clerkJSUrl,
@@ -61,14 +68,7 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] })
6168
scriptUrl={clerkJSScriptUrl(opts)}
6269
attributes={buildClerkJSScriptAttributes(opts)}
6370
dataAttribute='data-clerk-js-script'
64-
router={router}
6571
/>
66-
{/* Use <link rel='preload'> instead of <script> for the UI bundle.
67-
This tells the browser to download the resource immediately (high priority)
68-
but doesn't execute it, avoiding race conditions with __clerkSharedModules
69-
registration (which happens when React code runs @clerk/ui/register).
70-
When loadClerkUIScript() later adds a <script> tag, the browser uses the
71-
cached resource and executes it without re-downloading. */}
7272
{prefetchUI !== false && (
7373
<link
7474
rel='preload'

0 commit comments

Comments
 (0)