The Cloudflare flag that decides whether your Worker runs at all
If you deploy a static site to Cloudflare Workers with an [assets] binding, there is a
default you should know about before it costs you an afternoon.
The setup is ordinary. A static export lands in out/, the Worker serves it through the
assets binding, and the Worker also handles the couple of things static hosting can't — in
my case, collapsing www onto the apex so links and bookmarks converge on one URL.
The code is the obvious code:
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.hostname.startsWith("www.")) {
url.hostname = url.hostname.slice(4);
return Response.redirect(url.toString(), 301);
}
return env.ASSETS.fetch(request);
},
};
Deploy it. Test it. curl -I https://www.example.com/api/health returns a clean 301.
Ship it.
Then someone opens https://www.example.com/ and stays there.
The part that isn't in the code
By default, when a request matches a static asset, Cloudflare serves that asset before your Worker runs. Not after. Not alongside. Instead.
So the redirect works perfectly for /api/health, because there is no out/api/health
file to match. It never fires for /, or /about/, or any other real page — those all
have matching assets, and the Worker is simply never invoked.
This is a genuinely nasty failure shape, because the thing you reach for to test a
redirect is curl against some endpoint, and endpoints are exactly the paths that behave
correctly. The bug hides in the half of your URL space you're least likely to poke at from
a terminal.
The fix is one line in wrangler.toml:
[assets]
directory = "out"
binding = "ASSETS"
run_worker_first = true
Now the Worker runs first on every request, decides what it wants to own, and hands
everything else to env.ASSETS.fetch(request).
Why the default is the default
It's the right call for most sites. Serving a static asset without invoking a Worker is faster and cheaper, and the overwhelming majority of asset requests — every JS chunk, every font, every image — have no business waking up your code. Cloudflare optimized for the common case, which is what a good default does.
It's exactly wrong the moment your Worker owns something that has to be true for all requests. Canonical host. Auth. Geo-routing. Anything where "usually" is another word for "broken."
The general lesson is the one I keep relearning: when a platform hands you a fast path, find out what it skips. The performance win is advertised. The thing it skipped is in the docs somewhere, one paragraph, no bold text, and you will find it after the bug.