Next.js 15: CSP headers not applied in production unless await headers() is called #80997
-
SummaryI've encountered an issue with Next.js 15 where Content Security Policy (CSP) headers don't seem to be properly applied in production builds unless The Problem I'm ExperiencingDevelopment Mode (
|
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
HI, What you need is to make your page dynamic ~ https://nextjs.org/docs/app/guides/content-security-policy#adding-a-nonce-with-middleware Try with
I have merged a documentation update pointing to dynamic rendering, but it hasn't deployed to the docs site yet. Otherwise, a page gets opted-in to static rendering, and then the nonce is not applied to the scripts in the HTML send to the browser, cuz it ws already made during build time. Also I was testing out with |
Beta Was this translation helpful? Give feedback.
-
Based on the GitHub Discussions thread #80997, here's a concise summary of the key points: Issue OverviewUsers encounter WebSocket-related errors in Next.js (both development and production) when using libraries like Module not found: Can't resolve 'bufferutil'
Module not found: Can't resolve 'utf-8-validate' Why This Happens
Solutions1️⃣ Install Missing Modules (Quick Fix)npm install bufferutil utf-8-validate
# or
yarn add bufferutil utf-8-validate
2️⃣ Modify
|
Approach | Performance Impact | Security Level | Implementation |
---|---|---|---|
dynamic='force-dynamic' |
✅ High (per-request nonce) | Simple | |
Static CSP in config | ✅ Minimal | Moderate | |
Nonce in middleware only | 🛑 Broken | ❌ Low | Don't use |
Recommended Path
- Audit your routes
Identify pages with:grep -r 'nonce' ./app
- Apply selectively
// app/cart/page.tsx (example CSP-dependent page) export const dynamic = 'force-dynamic'
- Monitor performance
Check Vercel analytics or use:// page.tsx export const revalidate = 0 // Pair with dynamic for clarity
Advanced Options
For hybrid approaches:
// app/lib/nonce.ts
export function getCsp() {
if (process.env.NODE_ENV === 'development') {
return { nonce: crypto.randomBytes(16).toString('hex') }
}
return { hash: STATIC_HASH } // Build-time generated
}
Next Steps
- Calculate what % of routes truly need dynamic nonces
- Run Lighthouse tests comparing:
NEXT_PUBLIC_CSP_MODE=static npm run build NEXT_PUBLIC_CSP_MODE=dynamic npm run build
- Consider adding CSP report-uri to monitor violations
Would you like me to elaborate on any specific part? Particularly around performance measurement or hash generation?
Absolutely—let’s zoom in on both those areas. Here’s how you might tackle each:
-
Performance measurement deep-dive
• Leverage Next.js and Vercel analytics- Enable Next.js’ built-in Real-User Metrics or Vercel’s Insights dashboard to track TTFB, FCP, etc.
- Compare routes marked
force-dynamic
vs. static in a side-by-side view.
• Automated Lighthouse audits - Add a CI job that runs Lighthouse headlessly against your staging deploy:
name: Lighthouse CI on: [push] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Start Next.js run: npm ci && npm run start & npx wait-on http://localhost:3000 - name: Run Lighthouse run: | npm install -g @lhci/cli lhci autorun \ --collect.url=http://localhost:3000/cart \ --collect.url=http://localhost:3000/home \ --upload.target=temporary-public-storage
- Look at “Performance” and “Time to First Byte” scores for pages with/without dynamic CSP.
• Custom timing in your app - Use the Web Vitals API in
_app.tsx
to log or send metrics:import { reportWebVitals } from 'next/app'; export function reportWebVitals(metric) { console.log(metric.name, metric.value); // or send to your telemetry endpoint }
-
Build-time hash generation
• Node script to precompute SHA256 of your inline scripts// scripts/genCspHash.js import { readFileSync, writeFileSync } from 'fs'; import crypto from 'crypto'; const inlineScripts = readFileSync('app/nonce-scripts.js', 'utf-8'); const hash = crypto .createHash('sha256') .update(inlineScripts) .digest('base64'); writeFileSync('csp-hash.txt', hash);
• Wire it into your build
// package.json { "scripts": { "prebuild": "node scripts/genCspHash.js", "build": "next build" } }
• Use that hash in config
// next.config.js const staticHash = require('./csp-hash.txt').trim(); module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: `script-src 'self' 'sha256-${staticHash}'`, }, ], }, ]; }, };
Next, you might consider:
- Spinning up a tiny “canary” route with both approaches to see real-world latencies.
- Adding a CSP violation endpoint (Report-To/Report-URI) to catch misfires in production.
- Exploring a hybrid: dev uses nonces, prod uses hashes (controlled by
process.env.NODE_ENV
).
Let me know if you want a ready-to-go CI YAML for Lighthouse, a full sample repo structure, or pointers on setting up a CSP violation dashboard!
Beta Was this translation helpful? Give feedback.
-
Excellent additions! These pro-tips significantly elevate the solution approach. Let's synthesize them into a comprehensive strategy: Enhanced Solution Framework1. Webpack Externals (Optimized Client Bundling) // next.config.js
module.exports = {
webpack(config, { isServer }) {
if (!isServer) {
config.externals = [
...(config.externals || []),
'bufferutil',
'utf-8-validate',
...(process.env.NODE_ENV === 'production' ? ['ws'] : []) // Optional: exclude ws from client
];
}
return config;
},
}; Advantages:
2. Dynamic Import Strategy (Client Isolation) // components/SocketConnector.tsx
const SocketHandler = dynamic(
() => import('./SocketHandlerImpl').then(mod => mod.SocketHandler),
{
ssr: false,
loading: () => <ConnectionLoader />
}
); Best practices:
3. Architectural Separation Pattern src/
├── client/ # Browser-only code
├── server/ # Node-specific modules
├── shared/ # Universal utilities
└── lib/
├── socket-client.ts # export default class ClientSocket
└── socket-server.ts # export default class ServerSocket Import guards: // server/socket-server.ts
if (typeof window !== 'undefined') {
throw new Error('Server socket imported in client context!');
} 4. CI Pipeline Safeguards # .github/workflows/build.yml
- name: Check for forbidden imports
run: |
grep -r --include="*.ts*" -E "from 'ws'|import .* from 'bufferutil'" ./src/client && exit 1 || exit 0
- name: Analyze bundle
run: ANALYZE=true npm run build 5. Real-time Architecture Options graph LR
A[Browser] -->|WS| B[Vercel Edge]
B -->|Serverless| C[Upstash Redis]
C --> D[Next.js API Route]
D --> E[Database]
Recommended Path Forward
Your Decision Points:
Next Action Suggestions:
Which path resonates most with your architecture? Would you like to prioritize bundle optimization, real-time performance, or developer experience? |
Beta Was this translation helpful? Give feedback.
HI,
What you need is to make your page dynamic ~ https://nextjs.org/docs/app/guides/content-security-policy#adding-a-nonce-with-middleware
Try with
export const dynamic = 'force-dynamic'
I have merged a documentation update pointing to dynamic rendering, but it hasn't deployed to the docs site yet.
Otherwise, a page gets opted-in to static rendering, and then the nonce is not applied to the scripts in the HTML send to the browser, cuz it ws already made during build time.
Also I was testing out with
use client
components, and found an issue too.