nextjs - 💡(How to fix) Fix Docs: CSP nonce doesn’t work as expected in development due to DevTools/React Fast Refresh injecting inline styles without nonce [5 comments, 3 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#87343Fetched 2026-04-08 02:07:06
View on GitHub
Comments
5
Participants
3
Timeline
12
Reactions
1
Assignees
Timeline (top)
commented ×5assigned ×1closed ×1issue_type_added ×1

Error Message

Expected behavior: Next.js documentation should explicitly warn that:

Code Example

// middleware.ts
const isDev = process.env.NODE_ENV === 'development';
`style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com`;

---

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Define route groups - centralized access control
const routeGroups = {
    public: [
        "/",
        "/about",
        "/login",
        "/forgot-password",
        "/register"
    ],

    // Routes that require authentication
    authenticatedOnly: [
        "/feedback",
        "/services",
        "/polls",
        "/profile",
        "/profile/edit"
    ],

    // Routes accessible without authentication (but also with auth)
    publicOrAuthenticated: [
        "/feedback/report",
        "/services/report"
    ]
};

export function proxy(req: NextRequest) {
    const profile = req.cookies.get("profile")?.value;
    const currentPath = req.nextUrl.pathname;
    console.log("🚀 ~ middleware ~ currentPath:", currentPath);

    // Generate nonce for CSP
    const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

    // Get API URL from environment variable, add to connect-src
    const apiUrl = process.env.NEXT_PUBLIC_BASE_URL;
    const connectSrc = apiUrl
        ? `'self' ${apiUrl} https: wss: ws:`
        : "'self' https: wss: ws:";

    const isDevelopment = process.env.NODE_ENV === 'development';

    const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' ${isDevelopment ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com;
    img-src 'self' blob: data: https:;
    font-src 'self' https://fonts.gstatic.com data:;
    connect-src ${connectSrc};
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'self';
    ${!isDevelopment ? 'upgrade-insecure-requests;' : ''}
`;

    const contentSecurityPolicyHeaderValue = cspHeader
        .replace(/\s{2,}/g, ' ')
        .trim();

    let response: NextResponse;

    // ===============================
    // 1️⃣ Public routes - allow everyone
    // ===============================
    if (routeGroups.public.includes(currentPath)) {
        response = NextResponse.next();
    }
    // ===============================
    // 2️⃣ Public or authenticated routes - allow everyone
    // ===============================
    else if (routeGroups.publicOrAuthenticated.includes(currentPath)) {
        response = NextResponse.next();
    }
    // ===============================
    // 3️⃣ NOT authenticated
    // ===============================
    else if (!profile) {
        if (currentPath === "/feedback") {
            response = NextResponse.redirect(new URL("/feedback/report", req.url));
        } else if (currentPath === "/services") {
            response = NextResponse.redirect(new URL("/services/report", req.url));
        } else if (currentPath.startsWith("/polls") || currentPath.startsWith("/profile")) {
            response = NextResponse.redirect(new URL("/login", req.url));
        } else {
            response = NextResponse.rewrite(new URL("/not-found", req.url));
        }
    }
    // ===============================
    // 4️⃣ Authenticated
    // ===============================
    else if (profile) {
        if (routeGroups.authenticatedOnly.includes(currentPath) ||
            currentPath.startsWith("/feedback/") ||
            currentPath.startsWith("/services/") ||
            currentPath.startsWith("/polls") ||
            currentPath.startsWith("/profile")) {
            response = NextResponse.next();
        } else {
            response = NextResponse.rewrite(new URL("/not-found", req.url));
        }
    }
    // ===============================
    // 5️⃣ Not found
    // ===============================
    else {
        response = NextResponse.rewrite(new URL("/not-found", req.url));
    }

    // Add security headers to all responses
    response.headers.set('x-nonce', nonce);
    response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
    response.headers.set('X-Frame-Options', 'SAMEORIGIN');
    response.headers.set('X-Content-Type-Options', 'nosniff');
    response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    response.headers.delete('X-Powered-By');

    return response;
}

export const config = {
    matcher: [
        {
            source: '/((?!api|_next|_vercel|.*\\..*).*)',
            missing: [
                { type: "header", key: "next-router-prefetch" },
                { type: "header", key: "purpose", value: "prefetch" },
            ],
        },
    ],
};
RAW_BUFFERClick to expand / collapse

What is the documentation issue?

In dev mode (next dev), libraries like TanStack Query Devtools, Hot Toast libraries, and React internals inject <style> tags at runtime without including the CSP nonce — causing repeated style-src violations even when a valid nonce is set in the middleware.

✅ CSP nonce Works in production (after disabling Devtools or switching to 'unsafe-inline' in dev). ❌ CSP nonce Breaks in development when strict CSP like style-src 'self' 'nonce-...' is used — requiring hours of debugging.

Expected behavior: Next.js documentation should explicitly warn that:

nonce-based CSP is currently not fully compatible with dev tooling (e.g., React DevTools, Fast Refresh, library dev overlays). Recommend using 'unsafe-inline' for style-src in development only — and provide guidance on safely toggling it via NODE_ENV. Suggested fix/docs update: Add a ⚠️ CSP + Dev Mode section under Middleware / Security Headers, e.g.:

// middleware.ts
const isDev = process.env.NODE_ENV === 'development';
`style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com`;

After a full night of wanting my site to pass this test https://developer.mozilla.org/en-US/observatory

i ended up with this and i got 115/100

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Define route groups - centralized access control
const routeGroups = {
    public: [
        "/",
        "/about",
        "/login",
        "/forgot-password",
        "/register"
    ],

    // Routes that require authentication
    authenticatedOnly: [
        "/feedback",
        "/services",
        "/polls",
        "/profile",
        "/profile/edit"
    ],

    // Routes accessible without authentication (but also with auth)
    publicOrAuthenticated: [
        "/feedback/report",
        "/services/report"
    ]
};

export function proxy(req: NextRequest) {
    const profile = req.cookies.get("profile")?.value;
    const currentPath = req.nextUrl.pathname;
    console.log("🚀 ~ middleware ~ currentPath:", currentPath);

    // Generate nonce for CSP
    const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

    // Get API URL from environment variable, add to connect-src
    const apiUrl = process.env.NEXT_PUBLIC_BASE_URL;
    const connectSrc = apiUrl
        ? `'self' ${apiUrl} https: wss: ws:`
        : "'self' https: wss: ws:";

    const isDevelopment = process.env.NODE_ENV === 'development';

    const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' ${isDevelopment ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com;
    img-src 'self' blob: data: https:;
    font-src 'self' https://fonts.gstatic.com data:;
    connect-src ${connectSrc};
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'self';
    ${!isDevelopment ? 'upgrade-insecure-requests;' : ''}
`;

    const contentSecurityPolicyHeaderValue = cspHeader
        .replace(/\s{2,}/g, ' ')
        .trim();

    let response: NextResponse;

    // ===============================
    // 1️⃣ Public routes - allow everyone
    // ===============================
    if (routeGroups.public.includes(currentPath)) {
        response = NextResponse.next();
    }
    // ===============================
    // 2️⃣ Public or authenticated routes - allow everyone
    // ===============================
    else if (routeGroups.publicOrAuthenticated.includes(currentPath)) {
        response = NextResponse.next();
    }
    // ===============================
    // 3️⃣ NOT authenticated
    // ===============================
    else if (!profile) {
        if (currentPath === "/feedback") {
            response = NextResponse.redirect(new URL("/feedback/report", req.url));
        } else if (currentPath === "/services") {
            response = NextResponse.redirect(new URL("/services/report", req.url));
        } else if (currentPath.startsWith("/polls") || currentPath.startsWith("/profile")) {
            response = NextResponse.redirect(new URL("/login", req.url));
        } else {
            response = NextResponse.rewrite(new URL("/not-found", req.url));
        }
    }
    // ===============================
    // 4️⃣ Authenticated
    // ===============================
    else if (profile) {
        if (routeGroups.authenticatedOnly.includes(currentPath) ||
            currentPath.startsWith("/feedback/") ||
            currentPath.startsWith("/services/") ||
            currentPath.startsWith("/polls") ||
            currentPath.startsWith("/profile")) {
            response = NextResponse.next();
        } else {
            response = NextResponse.rewrite(new URL("/not-found", req.url));
        }
    }
    // ===============================
    // 5️⃣ Not found
    // ===============================
    else {
        response = NextResponse.rewrite(new URL("/not-found", req.url));
    }

    // Add security headers to all responses
    response.headers.set('x-nonce', nonce);
    response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
    response.headers.set('X-Frame-Options', 'SAMEORIGIN');
    response.headers.set('X-Content-Type-Options', 'nosniff');
    response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
    response.headers.delete('X-Powered-By');

    return response;
}

export const config = {
    matcher: [
        {
            source: '/((?!api|_next|_vercel|.*\\..*).*)',
            missing: [
                { type: "header", key: "next-router-prefetch" },
                { type: "header", key: "purpose", value: "prefetch" },
            ],
        },
    ],
};

Is there any context that might help us understand?

yes everything is there in description

Does the docs page already exist? Please link to it.

No response

extent analysis

TL;DR

To fix the Content Security Policy (CSP) issue with nonce-based style-src in development mode, use 'unsafe-inline' for style-src when NODE_ENV is 'development'.

Guidance

  • Identify if you are using a strict CSP policy with style-src 'self' 'nonce-...' in your development environment.
  • Update your CSP configuration to conditionally use 'unsafe-inline' for style-src when in development mode, as shown in the provided example code snippet.
  • Verify that your CSP policy is correctly set by checking the Content-Security-Policy header in your browser's developer tools.
  • Consider adding documentation to your project to highlight this potential issue and the recommended workaround for development environments.

Example

The provided code snippet already includes an example of how to conditionally set style-src based on the NODE_ENV:

const isDevelopment = process.env.NODE_ENV === 'development';
const cspHeader = `
    ...
    style-src 'self' ${isDevelopment ? "'unsafe-inline'" : `'nonce-${nonce}'`} https://fonts.googleapis.com;
    ...
`;

Notes

This workaround is specific to development environments and should not be used in production, where a strict CSP policy with a nonce is recommended for security.

Recommendation

Apply the workaround by using 'unsafe-inline' for style-src in development mode, as it allows for the use of development tools like React DevTools while maintaining a secure CSP policy in production.

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 - 💡(How to fix) Fix Docs: CSP nonce doesn’t work as expected in development due to DevTools/React Fast Refresh injecting inline styles without nonce [5 comments, 3 participants]