Skip to content

perf(platform): skip CSP/nonce middleware for hashed assets#2847

Closed
Israeltheminer wants to merge 1 commit into
mainfrom
perf/skip-csp-nonce-on-assets
Closed

perf(platform): skip CSP/nonce middleware for hashed assets#2847
Israeltheminer wants to merge 1 commit into
mainfrom
perf/skip-csp-nonce-on-assets

Conversation

@Israeltheminer

Copy link
Copy Markdown
Collaborator

Problem

Follow-up to the caching fix (#2843): repeat visits are now fast, but the first (cold) visit is still slow. Part of that is self-inflicted — on a cold load the ~150 content-hashed JS/CSS chunks each pass through the global secureHeaders middleware, which builds a full CSP string and a fresh crypto nonce per request, on the single Bun event loop. A static script/style byte stream never uses that nonce'd CSP, so it's wasted work paid ~150× during the burst. (Confirmed: on main, a request to /assets/vendor-katex-….js comes back with content-security-policy: default-src 'self'; script-src 'nonce-…'.)

Change

Bypass secureHeaders for content-hashed /assets/* in the app.use('*') guard — mirroring the existing /canvas-preview and /dav/* bypasses — and re-assert X-Content-Type-Options: nosniff on the static-file response (the one header that matters for a script/style stream; CSP/X-Frame/Referrer are document-level). Uses the same IMMUTABLE_ASSET pattern as the cache rule, so only hashed JS/CSS bypass; un-hashed images under /assets/ still get the full middleware.

Verification

  • bun run --filter @tale/platform test server.test.ts → green (57), incl. a new test asserting a hashed-asset response carries no CSP while /api/health still does. Verified it fails on main (asset gets the full CSP) and passes here.
  • typecheck clean; oxfmt --check + oxlint clean.

Honest caveat — this is measure-after

I can't prove from the HAR alone that this middleware is the cold-load bottleneck (the box's raw bandwidth and Caddy's on-the-fly gzip/zstd are also candidates). It's a correct optimization regardless (don't build a per-request nonce for static bytes). Next step: deploy, capture a fresh cold HAR, and measure first-load TTFB/throughput. If it's not enough, the documented escalation is moving /assets/* to Caddy file_server (bigger infra change; see #2842).

On a cold load ~150 content-hashed JS/CSS chunks each ran the global secureHeaders middleware (a per-request CSP build + crypto nonce) on the single Bun event loop, though a static script/style response never uses the nonce'd CSP. Bypass secureHeaders for content-hashed /assets/* (mirroring the existing canvas-preview and /dav guards) and re-assert X-Content-Type-Options: nosniff on the file response — the one header that matters for a script byte stream.
@Israeltheminer

Copy link
Copy Markdown
Collaborator Author

Closing — net negative on review.

Security: this introduces a clickjacking hole. Skipping the security-headers middleware by URL pattern means a forged /assets/<hash>.js path (matches the pattern, no real file) falls through to index.html, served as 200 text/html with no CSP and no X-Frame-Options. Reproduced against a simulated production dist.

Benefit doesn't hold up: the per-request CSP nonce it avoids costs microseconds — it can't explain the ~20s cold-load TTFB. That bottleneck is delivery/infra (#2842), not this middleware.

Keeping the security headers intact on every path. Cold-load will be pursued via the Caddy file_server offload (#2842) with real measurement. The merged wins (#2843 caching, #2846 hash-length) are unaffected.

@Israeltheminer
Israeltheminer deleted the perf/skip-csp-nonce-on-assets branch July 22, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant