{"slug": "next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval", "title": "`next dev` Renders but Nothing Works: Your CSP Is Missing `unsafe-eval`", "summary": "A developer of AI Change Watch, a small independent project, discovered that their Content-Security-Policy (CSP) was breaking all client-side interactivity in Next.js development mode. The issue arises because Next.js dev server uses eval for module compilation and React Refresh, which requires 'unsafe-eval' in the CSP, while production builds do not. The developer offers two solutions: conditionally widening the CSP in development or testing client behavior with a production build.", "body_md": "I run [ AI Change Watch](https://aichangewatch.com/?src=devto), a small independent project that\n\nAt some point I added a Content-Security-Policy. It was correct. It shipped. Production was fine.\n\nAnd then, locally, every interactive thing on the site stopped working.\n\n`next dev`\n\nstarts. The page loads. It looks **exactly right** — the layout, the data, the styles, all\n\nof it. Then:\n\n`onClick`\n\nanywhere firesNo error page. No red overlay. No failed request in the Network tab. The server rendered the HTML and\n\nsent it, so the page you are looking at is real — it is just **completely inert**. Nothing hydrated.\n\nIf you have not hit this before, the natural first guess is your own component. That is where I went,\n\nand it is the wrong place, because every component is fine.\n\nThe console has it, but you have to be looking:\n\n```\nRefused to evaluate a string as JavaScript because 'unsafe-eval' is not an\nallowed source of script in the following Content Security Policy directive:\n\"script-src 'self' 'unsafe-inline' …\"\n```\n\nAnd the reason it is easy to miss is that it is not a JavaScript error. It does not have a stack. It\n\ndoes not point at your file. It appears once, near the top, above whatever else the page logged, and it\n\nnames a directive rather than a component.\n\nNext's development server compiles modules and hands them to the browser wrapped in `eval`\n\n— that is\n\nhow the dev `devtool`\n\nsetting works, and it is what React Refresh needs to swap a component without\n\nreloading the page. Fast Refresh is built on it.\n\nA production build does not do that. `next build`\n\nemits static chunks. There is no string being\n\nevaluated at runtime, so there is nothing for `'unsafe-eval'`\n\nto permit.\n\nWhich produces the trap:\n\nThe CSP is correct for production and fatal in development — and development is where you spend all\n\nyour time.\n\nYou will not catch it in CI, because CI builds. You will not catch it in preview, because preview\n\nbuilds. You catch it the moment you try to click something locally, and by then you are three commits\n\ninto a feature and looking for the bug in your own diff.\n\nBecause it is in `next.config`\n\n, and that file has no idea which mode it is running in unless you tell\n\nit:\n\n```\n// next.config.mjs\nasync headers() {\n  return [{ source: '/:path*', headers: securityHeaders }];\n},\n```\n\n`source: '/:path*'`\n\nmeans every path. There is no dev/prod branch, so `next dev`\n\nserves the same header\n\n`next start`\n\ndoes. That is a reasonable default — you generally *want* to develop against the headers\n\nyou ship — it just happens to be wrong for this one directive.\n\n**Option A — widen the policy in development only.**\n\n``` js\nconst isDev = process.env.NODE_ENV === 'development';\n\nconst csp = [\n  \"default-src 'self'\",\n  // 'unsafe-eval' is DEV-ONLY: the dev server evaluates compiled modules as strings (that is what\n  // React Refresh is built on), and a production build never does. Shipping it would be a real\n  // widening of the policy for zero benefit.\n  `script-src 'self' 'unsafe-inline'${isDev ? \" 'unsafe-eval'\" : ''}`,\n  // The HMR socket, same reasoning. In production nothing connects back to the dev server.\n  `connect-src 'self'${isDev ? ' ws: wss:' : ''}`,\n  // …the rest\n].join('; ');\n```\n\nThe comment is not decoration. A conditional in a security header is exactly the kind of line that gets\n\n\"cleaned up\" six months later by someone who reads it as an inconsistency, so the reason it is\n\nconditional has to sit next to it.\n\n**Option B — stop testing client behaviour in next dev.**\n\n```\nnext build && next start\n```\n\nThis is what I actually do, for a reason that has nothing to do with CSP: this project deploys to\n\nCloudflare Workers through OpenNext, and `next dev`\n\nis not the runtime it ships on. Behaviour I verify\n\nin dev is behaviour I verified somewhere the code will never run. So for anything client-side I build\n\nand serve the real thing.\n\nThe cost is real — you lose Fast Refresh, and a rebuild per change is slow enough to change how you\n\nwork. If your production runtime *is* Node, Option A is the better trade. If it isn't, Option B was\n\ngoing to be necessary anyway and this just makes it obvious sooner.\n\nThe thing worth taking away isn't the directive. It's this:\n\nA security header set in`next.config`\n\napplies to the dev server, and the dev server has different\n\nrequirements than the thing you deploy.\n\n`unsafe-eval`\n\nis the one that produces a *silent* failure, which is why it costs the most time. But the\n\nsame category catches you elsewhere:\n\n| directive | what dev needs that prod doesn't |\n|---|---|\n`script-src` |\n`'unsafe-eval'` for the module runtime / React Refresh |\n`connect-src` |\n`ws:` / `wss:` for the HMR socket |\n`style-src` |\n`'unsafe-inline'` if your prod build extracts CSS but dev injects it |\n\nIf you are about to add a CSP to a Next app, the fastest check is not a code review. It is:\n\n`next dev`\n\nThirty seconds, and it is the only test that distinguishes \"rendered\" from \"working\". Everything else\n\nabout a dead page looks identical to a live one.\n\n*The tracker this came out of is at aichangewatch.com — it watches AI\nvendor docs for changes. Its CSP still has no 'unsafe-eval' in production, which is the point.*", "url": "https://wpnews.pro/news/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval", "canonical_source": "https://dev.to/ai_changewatch/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval-26pl", "published_at": "2026-08-24 12:00:00+00:00", "updated_at": "2026-08-24 12:13:32.230691+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["AI Change Watch", "Next.js", "React Refresh", "Cloudflare Workers", "OpenNext"], "alternates": {"html": "https://wpnews.pro/news/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval", "markdown": "https://wpnews.pro/news/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval.md", "text": "https://wpnews.pro/news/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval.txt", "jsonld": "https://wpnews.pro/news/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval.jsonld"}}