Skip to content

fix(cdn): enforce Content-Type on R2 CDN objects (@latest worker + publish script)#31

Merged
whykusanagi merged 2 commits into
mainfrom
fix/cdn-worker-content-type
Jun 1, 2026
Merged

fix(cdn): enforce Content-Type on R2 CDN objects (@latest worker + publish script)#31
whykusanagi merged 2 commits into
mainfrom
fix/cdn-worker-content-type

Conversation

@whykusanagi
Copy link
Copy Markdown
Owner

@whykusanagi whykusanagi commented Jun 1, 2026

Problem

R2 objects under corrupted-theme/@<ver>/dist/ are served with no Content-Type because scripts/publish-to-cdn.sh uploads them via wrangler r2 object put, which doesn't set MIME. A <link rel=stylesheet> in standards mode refuses a stylesheet that isn't text/css, silently breaking CDN consumers — the blocker for the nikkers.cc unpkg→R2 cutover.

This is not an npm-package bug: the package ships src/ only; cdn-worker/ and dist/ are excluded. It only ever affected CDN consumers, never npm install consumers.

Fix (two layers, same extension→type table)

1. cdn-worker/index.js — fixes @latest. The Worker passed the upstream object through verbatim (new Response(upstream.body, upstream)). It now maps file extension → Content-Type and stamps it on every response, overriding any missing/text-plain upstream type. Because the Worker is the single choke point every @latest request flows through, this needs no edge-cache purge — the header is re-applied even when the upstream body is served from cache (verified against a stale HIT age:4878 entry).

2. scripts/publish-to-cdn.sh — fixes direct @<version>/ paths. The Worker only covers @latest; a consumer pinning an exact version (@0.2.1/dist/theme.min.css) is served straight from R2 and would hit the same rejection. The script now passes --content-type (by extension) and --cache-control: public, max-age=31536000, immutable on every dist/ and data/ upload, via a shared content_type_for() table kept in sync with the Worker. Unknown extensions warn rather than silently shipping an untyped object.

Verification

  • Worker deployed (corrupted-theme-cdn, version addfdefc). All @latest/dist/* on both cdn.nikkers.cc and cdn.whykusanagi.xyz return correct types, fresh and cached.
  • Script: bash -n + shellcheck clean; content_type_for() unit-checked across css/js/mjs/json/map/svg/png/woff2/txt.
File Content-Type
theme.min.css / nikke-utilities.css text/css; charset=utf-8
corrupted-text.global.js / timer-registry.global.js text/javascript; charset=utf-8
data/*.json application/json; charset=utf-8

Follow-ups (out of scope, flagged)

  • s3.whykusanagi.xyz is not in the Worker routes, so s3/@latest/* still serves text/plain. Not blocking nikke (it uses cdn.nikkers.cc); add the route if anything consumes the raw bucket host's @latest.
  • The build (npm run build / build:umd) does not yet generate nikke-utilities.css or corrupted-text.global.js into dist/ — they were published manually for 0.2.1. The publish script uploads whatever is in dist/, so a future release needs those wired into the build or they'll be missing again.

🤖 Generated with Claude Code

R2 objects uploaded via `wrangler r2 object put` (the path used by
scripts/publish-to-cdn.sh) carry no Content-Type. The @latest Worker
passed that through verbatim with `new Response(upstream.body, upstream)`,
so dist/theme.min.css and friends were served with a missing/text-plain
type. A <link rel=stylesheet> in standards mode refuses CSS that isn't
text/css, which silently broke CDN consumers (nikkers.cc cutover blocker).

Map file extension -> Content-Type in the Worker and stamp it on every
@latest response, overriding the upstream value. The Worker is the single
choke point every @latest request flows through, so this fixes the whole
class of bug regardless of how the underlying objects were uploaded, and
without needing edge-cache purges (the header is re-applied even when the
upstream body is served from cache).

Verified on cdn.nikkers.cc and cdn.whykusanagi.xyz: all @latest/dist/*
now return text/css / text/javascript, fresh and cached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 1, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corrupted-theme d6a3b7a Commit Preview URL

Branch Preview URL
Jun 01 2026, 05:37 AM

The Worker fixes Content-Type for @latest, but direct @<version>/ paths
are served straight from R2 and still lacked a type, because this script
uploaded via `wrangler r2 object put` with no --content-type. A consumer
pinning an exact version (e.g. @0.2.1/dist/theme.min.css) would hit the
same standards-mode <link> rejection the Worker change avoids for @latest.

Add a content_type_for() extension map (kept in sync with
cdn-worker/index.js) and a put_object() helper that passes --content-type
and --cache-control (public, max-age=31536000, immutable) on every dist/
and data/ upload. Unknown extensions warn rather than silently shipping
an untyped object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@whykusanagi whykusanagi changed the title fix(cdn-worker): enforce Content-Type by extension on @latest responses fix(cdn): enforce Content-Type on R2 CDN objects (@latest worker + publish script) Jun 1, 2026
@Rith-Portfolio
Copy link
Copy Markdown

src/
├── components/
│ ├── Cursor.jsx
│ ├── ParticlesBg.jsx
│ ├── Projects.jsx
│ ├── Contact.jsx
├── App.jsx
├── main.jsx
└── styles.css

@whykusanagi whykusanagi merged commit 9c71750 into main Jun 1, 2026
5 checks passed
@whykusanagi whykusanagi deleted the fix/cdn-worker-content-type branch June 1, 2026 23:36
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.

2 participants