Nonce-based CSP with inline <head> scripts is incompatible with cacheComponents
@icyJoseph is already working on this.
Since Aug 4, 2026.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.5k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/r34son/nextjs-cc-repro
To Reproduce
- npm run dev
- npm run build
Current vs. Expected behavior
Description
When using cacheComponents: true with nonce-based Content Security Policy (CSP), there is no way to render inline scripts in <head> that must execute before any DOM elements appear.
Background
A common pattern for CSP is generating a per-request nonce in the proxy (middleware), forwarding it via an x-nonce header, then reading it in the root layout with headers() to attach the nonce attribute to inline scripts. The official Next.js CSP documentation recommends exactly this approach.
Many applications have inline scripts in <head> that must execute synchronously before the DOM renders — polyfills (e.g. dvh viewport unit), theme detection (prevent flash of wrong theme), critical analytics bootstrapping, etc.
The Problem
With cacheComponents: true, calling headers() in the root layout is uncached dynamic data access. This creates two conflicting requirements:
Option 1: Call headers() at the layout top level (outside <Suspense>)
// app/layout.tsx
export default async function RootLayout({ children }) {
const nonce = (await headers()).get('x-nonce') ?? '';
return (
<html>
<head>
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: '...' }} />
</head>
<body>{children}</body>
</html>
);
}
Result: Build fails with:
Error: Route "/_not-found": Uncached data was accessed outside of <Suspense>.
Every page wrapped by this layout fails to prerender, including /_not-found.
Option 2: Wrap the headers() call in <Suspense>
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<Suspense>
<NonceScript /> {/* async component that reads headers() */}
</Suspense>
</head>
<body>{children}</body>
</html>
);
}
Result: Build succeeds, but the inline script is streamed in via React's hydration mechanism after the initial HTML shell. It no longer blocks rendering. A <head> polyfill that must run before any DOM paint is now injected late, defeating its purpose entirely.
Neither option works
| Approach | Build | Script timing |
|---|---|---|
headers() outside <Suspense> |
Fails | Would be correct (blocking) |
headers() inside <Suspense> |
Succeeds | Wrong (streamed late) |
Workaround
Use sha256- hash-based CSP exceptions for static inline scripts instead of nonces. Compute the hash in the proxy and add it to the CSP header alongside the nonce:
script-src 'self' 'nonce-${nonce}' 'sha256-${hash}' 'strict-dynamic';
This removes the need for headers() in the layout. The nonce still covers framework scripts (applied automatically by Next.js from the CSP header), and the hash covers the static inline script.
However, this workaround doesn't help when:
- The inline script content is dynamic (e.g. contains server-injected values)
- You need the nonce for third-party
<Script>components in<head>withbeforeInteractivestrategy - You have multiple inline scripts that change frequently (maintaining hashes is fragile)
What about Subresource Integrity (SRI)?
The CSP docs mention experimental SRI support as an alternative to nonces. SRI generates sha256 hashes of JavaScript files at build time and adds integrity attributes to script tags, allowing static generation while maintaining a strict CSP.
However, SRI does not solve this problem:
- SRI only works for external scripts — it hashes the contents of JavaScript files. It cannot be applied to inline scripts rendered via
dangerouslySetInnerHTML, which is the exact use case here. - SRI is experimental and webpack-only — it does not work with Turbopack, which is the default bundler in Next.js 16.
- SRI is build-time only — it cannot handle dynamically generated scripts.
So SRI is not an alternative for the inline <head> script + cacheComponents scenario.
Expected Behavior
There should be a way to use nonce-based CSP for blocking inline <head> scripts while cacheComponents is enabled. Possible solutions:
- Allow the layout to opt out of the "no uncached data outside Suspense" rule for the
/_not-foundprerender specifically - Provide a built-in mechanism to access the nonce without calling
headers()(e.g. auseNonce()hook or automatic nonce injection fordangerouslySetInnerHTMLscripts, similar to how framework scripts are handled) - Support
<Suspense>in<head>with blocking semantics (wait for resolution before flushing the<head>, rather than streaming)
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.2.0: Tue Nov 18 21:07:05 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6020
Available memory (MB): 32768
Available CPU cores: 12
Binaries:
Node: 22.18.0
npm: 10.9.3
Yarn: 1.22.22
pnpm: 10.18.3
Relevant Packages:
next: 16.1.5 // Latest available version is detected (16.1.5).
eslint-config-next: N/A
react: 19.2.3
react-dom: 19.2.3
typescript: 5.9.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
cacheComponents
Which stage(s) are affected? (Select all that apply)
next build (local)
Additional context
@ztanner @icyJoseph Hi Zack and Joseph, could you weigh in on this? We're running into a situation where nonce-based CSP and cacheComponents are fundamentally incompatible for inline <head> scripts that need to block rendering.
Is there a recommended pattern we're missing, or is this a known gap? Would love to hear if there are plans to support something like automatic nonce injection for custom inline scripts (similar to how framework scripts already get the nonce from the CSP header automatically). Thanks!
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.