The Symptom
I run a per-request nonce CSP on every client site. Middleware generates a nonce, bakes it into script-src 'nonce-…' 'strict-dynamic', and SolidStart attaches the same nonce to its inline hydration scripts. One day I curled a site and the response header said something else entirely:
content-security-policy: ... script-src 'self' 'unsafe-inline' ...
Meanwhile the HTML body carried fresh <script nonce="..."> values that rotated on every request. Two systems were fighting. The nonce machinery was running and producing nonces nobody enforced, while a weaker static CSP with 'unsafe-inline' was actually shipping to browsers. That static policy turned out to be a Cloudflare Transform Rule I had added months earlier "temporarily" -- and it was masking the real failure.
The Mechanism
The headers were being set in SolidStart's onBeforeResponse middleware hook, which seems like the obvious place: build the response, then decorate it with headers. The problem is that for streamed SSR, onBeforeResponse cannot mutate response headers at all. Its second argument is { body? }, not the Response object. Anything you write to headers there is silently discarded, with no error and nothing in the logs. The page renders fine, hydration works, and the only evidence is a missing header you have to go looking for.
With the Worker's CSP never attaching, the Transform Rule's static policy filled the vacuum. Every page looked "protected" in a scanner, so nothing flagged it.
The Fix
Headers move to the onRequest hook and get set on the H3 native event:
const nativeEvent = event.nativeEvent;
for (const [key, value] of Object.entries(buildSecurityHeaders(nonce))) {
setResponseHeader(nativeEvent, key, value);
}
The nonce is generated in the same hook, stashed on event.locals.nonce, and wired into SolidStart's hydration context in entry-server.tsx:
export default createHandler(
() => <StartServer document={...} />,
(event) => ({ nonce: event.locals.nonce }),
);
Components that need the value read it with useNonce(). Because the CSP value embeds the nonce, the header map is built fresh per request, never memoized as a module constant. I also deleted the Transform Rule entirely; disabling it would have left the trap armed for the next config change.
Verification
The only check I trust is two raw curls against production:
curl -sI https://valkyrienexus.com/
curl -sI https://valkyrienexus.com/
Then fetch the body and compare. Three things must hold: the nonce-… value in the CSP header equals the nonce on the inline <script> tags in the same response, the nonce rotates between the two requests, and script-src contains no 'unsafe-inline'. If the header nonce and body nonce disagree, or the header shows 'unsafe-inline' while the body carries per-request nonces, the Worker CSP isn't attaching and something static is masking it.
A scanner grade will not catch this. The scanner sees a CSP header and scores it; it does not cross-check the header against the inline scripts in the same response.
The Rule I Took Away
Security headers live in code, full stop. The moment a Transform Rule or dashboard toggle also sets them, you have two sources of truth, and the wrong one wins silently. I audited the whole fleet on 2026-06-04 -- stonefyr.com, airassaultfireworks.com, arnoldsappliancerepair.com, valkyrienexus.com, valkyrienexus.dev -- checking headers and body from the same request on each. All five now serve the Worker's nonce CSP, and the only thing left in the Cloudflare dashboard is TLS settings and redirects.
The trade-off is that every new site needs the middleware port (six files: middleware.ts, security-headers.ts, nonce.ts, entry-server.tsx, the CSP report endpoint, and app.config.ts). That is more setup than clicking a Transform Rule into existence. It is also the only version that actually ships.