Skip to content

BuildNot yet confirmed elsewhere1 publisher3 min readPublished

A strict CSP killed one form in production, and every monitor stayed green

A per-request nonce met a statically prerendered page, and the browser refused every script it was sent. Six health signals, including a 3,500-test suite, reported fine.

The Engineer · Build desk

How we use AISend a correction

Illustration accompanying A strict CSP killed one form in production, and every monitor stayed green
Generated illustration

What happened

  • Middleware stamped a fresh nonce into a script-src carrying strict-dynamic, with no unsafe-inline for scripts.
  • The build route table showed /forgot-password prerendered at build time while /login and /reset-password rendered per request.
  • The browser refused to execute main-app.js, layout.js and every other chunk on that page, citing the site's own policy.

Why it matters

  • cost The bill was paid by users who could not reset a password, with a support email as the only detector, for as long as that audit log had existed.
  • constraint Monitoring keyed to requests cannot cover this class at all, because the failure completes before anything is sent; the instrument has to be a browser running the production bundle under the...
  • exposure Every interactive route that the build marks as prerendered is exposed the same way, which makes the route table, not the dashboard, the list of pages that might already be dead.
  • decision Leaving the fix as a per-file export means each new page is another chance to forget it, so the choice is now between an author's memory and a check that runs on the build.

A nonce is worth something only because it is minted per request, and a page baked at build time has no request to mint from, so the framework has nothing to stamp onto the script tags it writes [5][6]. It does not fail loudly at that point. It emits the tags bare, and the middleware still attaches a header demanding a nonce to the response carrying them [15]. With no unsafe-inline for scripts anywhere in the policy, an un-nonced tag had nothing else to match against, so the browser blocked everything from main-app.js down [14][16].

The invisibility comes from what the HTML does next. React never hydrated, so no component mounted; the bot-check widget, which arrives by script, never drew, and the submit handler never attached [17]. A button inside a form with no handler still does its native job, which is submit and reload, so the user got a flicker and the same page back [18]. No request of any kind left the browser [19].

Six health signals were green, and every one of them samples the server: the error tracker, the server logs [1], the password-reset audit trail [2], curl against the endpoint, the page's 200 [3], and 3,500 passing tests [4][21]. None of them can see a failure that never leaves the client. The audit log deserves a second look, though, because it does not say "no recent requests". It records every password-reset request and holds zero, ever, which means the form had produced nothing at all for the entire life of that log [2][22]. This was not something that broke last Tuesday.

Finding it took about twenty characters in a console. Counting scripts that carry a nonce attribute returned 15 on /login, 19 on /pricing, and 0 on /forgot-password [20]. The gap is not marginal: the lowest healthy count sits fifteen above the dead one, so "greater than zero" is a clean pass or fail for any route in the build [24].

The repair is a single export in the page file, dynamic set to force-dynamic [11]. Its sibling /reset-password already had that line, because someone added it there once and did not add it to the twin page [12]. That is the part worth being unhappy about. Under a nonce policy, whether a form works at all is decided by whether an author remembered a per-file annotation, and the build output already prints the answer: any route flagged as statically prerendered that also ships interactivity is a dead route waiting for a user to notice [23]. Reading that table costs nothing next to the thing that actually caught this, which was a real browser pointed at the production build with the production header and the console open [10].

What to watch

  • Whether Next.js starts failing the build, rather than shipping un-nonced scripts, when a prerendered route is served under a nonce policy.
  • Whether the nonced-script count becomes a post-deploy smoke check run in a real browser against the production build and header.
  • Whether the per-page force-dynamic annotation is replaced by a build-time rule covering every interactive route.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence60
Adoption20
Hype gap+8
Incentives30
Confidence55
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    The team checked the error tracker and the server logs after the report; both were clean.

  2. [2]

    The audit log that records every password-reset request showed zero requests, ever.

  3. [3]

    The endpoint worked perfectly from curl and the page returned a healthy 200.

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 27, 2026

    The CSP that killed our own form (a production-only Next.js bug)

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Loading related stories