fix(cdn): enforce Content-Type on R2 CDN objects (@latest worker + publish script)#31
Merged
Merged
Conversation
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>
Deploying with
|
| 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>
|
src/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
R2 objects under
corrupted-theme/@<ver>/dist/are served with noContent-Typebecausescripts/publish-to-cdn.shuploads them viawrangler r2 object put, which doesn't set MIME. A<link rel=stylesheet>in standards mode refuses a stylesheet that isn'ttext/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/anddist/are excluded. It only ever affected CDN consumers, nevernpm installconsumers.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-Typeand stamps it on every response, overriding any missing/text-plainupstream type. Because the Worker is the single choke point every@latestrequest 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 staleHIT age:4878entry).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, immutableon everydist/anddata/upload, via a sharedcontent_type_for()table kept in sync with the Worker. Unknown extensions warn rather than silently shipping an untyped object.Verification
corrupted-theme-cdn, versionaddfdefc). All@latest/dist/*on bothcdn.nikkers.ccandcdn.whykusanagi.xyzreturn correct types, fresh and cached.bash -n+shellcheckclean;content_type_for()unit-checked across css/js/mjs/json/map/svg/png/woff2/txt.theme.min.css/nikke-utilities.csstext/css; charset=utf-8corrupted-text.global.js/timer-registry.global.jstext/javascript; charset=utf-8data/*.jsonapplication/json; charset=utf-8Follow-ups (out of scope, flagged)
s3.whykusanagi.xyzis not in the Workerroutes, sos3/@latest/*still servestext/plain. Not blocking nikke (it usescdn.nikkers.cc); add the route if anything consumes the raw bucket host's@latest.npm run build/build:umd) does not yet generatenikke-utilities.cssorcorrupted-text.global.jsintodist/— they were published manually for 0.2.1. The publish script uploads whatever is indist/, so a future release needs those wired into the build or they'll be missing again.🤖 Generated with Claude Code