nextjs - ✅(Solved) Fix Nonce-based CSP with inline <head> scripts is incompatible with cacheComponents [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#89754Fetched 2026-04-08 00:21:33
View on GitHub
Comments
0
Participants
1
Timeline
11
Reactions
7
Author
Participants
Timeline (top)
subscribed ×5mentioned ×2cross-referenced ×1issue_type_added ×1

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.

Error Message

Error: Route "/_not-found": Uncached data was accessed outside of <Suspense>.

Root Cause

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.

Fix Action

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> with beforeInteractive strategy
  • You have multiple inline scripts that change frequently (maintaining hashes is fragile)

PR fix notes

PR #724: perf: fix critical query limits, static optimization, render perf, and navigation prefetch

Description (problem / solution / changelog)

  • #693: Add .limit(channelIds.length) to DM channels latest messages query
  • #694: Add .limit(10000) to search attachment candidate query
  • #695: Remove force-dynamic from root layout; apply per-layout to channels/auth/settings only
  • #697: Debounce resize handler, conditional typing interval, convert animatedMessageIds to ref, memoize buildComponents
  • #698: Parallelize push notification settings queries with Promise.all
  • #699: Convert server icons to Link with prefetch, add prefetch links to channel items

https://claude.ai/code/session_013krgMXBWXJtCRN9AXZxGgv

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

Summary by CodeRabbit

  • Performance & Optimization

    • Optimized message query limits for improved performance in direct messages and search functionality
    • Enhanced chat area rendering efficiency and reduced unnecessary re-renders
    • Improved navigation prefetching for faster page transitions between channels and servers
    • Optimized notification settings retrieval to use concurrent requests
  • Refactor

    • Updated navigation components to leverage Next.js link optimization for better performance
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Changed files

  • apps/web/app/api/dm/channels/route.ts (modified, +2/-1)
  • apps/web/app/api/search/route.ts (modified, +2/-0)
  • apps/web/app/channels/layout.tsx (modified, +2/-0)
  • apps/web/app/layout.tsx (modified, +6/-13)
  • apps/web/app/settings/layout.tsx (modified, +2/-0)
  • apps/web/components/chat/chat-area.tsx (modified, +26/-26)
  • apps/web/components/layout/channel-sidebar.tsx (modified, +1/-0)
  • apps/web/components/layout/server-sidebar.tsx (modified, +7/-5)
  • apps/web/components/layout/sortable-channel-item.tsx (modified, +6/-0)
  • apps/web/lib/push.ts (modified, +33/-29)

PR #444: fix(hydration): plain <script src> to clear CSP nonce mismatch

Description (problem / solution / changelog)

Summary

  • <Script strategy="beforeInteractive"> (from next/script) emits an inline bootstrap shim even with an external src=. Turbopack decorates that inline tag with a CSP nonce; browsers strip the attribute after validation, leaving a server:nonce="" ↔ client:nonce=undefined hydration diff that fires "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties" on every first page load in Playwright (visible in auth-setup, 12 lines of log noise per run).
  • Swap the two <Script> tags in src/app/layout.tsx for plain <script src="…"> — no inline content, no nonce attribute, no diff. Both still run before first paint (runtime-config bootstrap + theme-init FOUC prevention).
  • Same approach as the unmerged origin/fix/script-nonce-hydration branch (cb782c8). PR #245 tried to fix this in Feb but ended on <Script> + external src, which still produces the inline shim.

Upstream context: vercel/next.js#77952, vercel/next.js#89754

Test plan

  • pnpm exec playwright test tests/auth.setup.ts — 1/1 pass, 0 warnings (was 12 lines of nonce diff per run)
  • pnpm exec playwright test tests/flame.spec.ts tests/people.spec.ts — 7/7 pass, 0 hydration/nonce warnings
  • pnpm typecheck clean
  • pnpm lint clean (@next/next/no-sync-scripts disabled per-line with inline rationale; perf trade-off is intentional for pre-paint bootstrap)
  • Reviewer sanity-check: verify /runtime-config.js and /theme-init.js still execute before hydration (view-source → scripts are first children of <body>)

Changed files

  • src/app/layout.tsx (modified, +13/-3)

Code Example

// 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>
  );
}

---

Error: Route "/_not-found": Uncached data was accessed outside of <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>
  );
}

---

script-src 'self' 'nonce-${nonce}' 'sha256-${hash}' 'strict-dynamic';

---

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
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/r34son/nextjs-cc-repro

To Reproduce

  1. npm run dev
  2. 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

ApproachBuildScript timing
headers() outside <Suspense>FailsWould be correct (blocking)
headers() inside <Suspense>SucceedsWrong (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> with beforeInteractive strategy
  • 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:

  1. Allow the layout to opt out of the "no uncached data outside Suspense" rule for the /_not-found prerender specifically
  2. Provide a built-in mechanism to access the nonce without calling headers() (e.g. a useNonce() hook or automatic nonce injection for dangerouslySetInnerHTML scripts, similar to how framework scripts are handled)
  3. 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!

extent analysis

Problem Summary

The problem is that using cacheComponents: true with nonce-based Content Security Policy (CSP) prevents rendering inline scripts in <head> that must execute before any DOM elements appear.

Root Cause Analysis

The root cause is the conflict between cacheComponents: true and nonce-based CSP. When cacheComponents: true, calling headers() in the root layout is uncached dynamic data access, which creates a conflict with the nonce-based CSP.

Fix Plan

To fix this issue, we need to find a way to access the nonce without calling headers() or use a different approach that doesn't require nonce-based CSP. Here are a few possible solutions:

Solution 1: Use sha256- hash-based CSP exceptions

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.

// app/layout.tsx
export default async function RootLayout({ children }) {
  const nonce = (await headers()).get('x-nonce') ?? '';
  const hash = computeHash('script-content');
  return (
    <html>
      <head>
        <script nonce={nonce} integrity={hash} src="script.js" />
      </head>
      <body>{children}</body>
    </html>
  );
}

function computeHash(content: string): string {
  // Compute the hash of the script content
  const hash = crypto.createHash('sha256');
  hash.update(content);
  return hash.digest('hex');
}

Solution 2: Use a different approach for inline scripts

Use a different approach for inline scripts that don't require nonce-based CSP. For example, you can use a separate script tag for each inline script and add the nonce to each script tag.

// app/layout.tsx
export default async function RootLayout({ children }) {

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix Nonce-based CSP with inline <head> scripts is incompatible with cacheComponents [2 pull requests, 1 participants]