I'm working now on an application to deploy as a cloudflare worker. It's a jamstack which uses some static pages with apis. I want to restrict access to those pages to allow only to logged users.
I was trying all kind of options until I run into run_worker_first
. run_worker_first
is exactly the knob to use to make your Worker intercept requests before the static-asset handler, so you can do auth checks on selected paths.
What run_worker_first
does
- Default behavior: assets are matched/served first; your Worker only runs for misses.
-
With
run_worker_first
: your Worker runs first (for all routes, or only the ones you choose). That lets you check cookies/JWT, hit KV/R2, then decide whether to serve fromenv.ASSETS.fetch(request)
or block/redirect. (Cloudflare Docs)
Minimal config (only gate certain paths)
If you only need to protect, say, /assets/protected/*
and /documents/*
, you can keep everything else “asset-first”:
// wrangler.jsonc
{
"name": "my-app",
"main": "src/worker.ts",
"compatibility_date": "2025-10-17",
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": ["/assets/protected/*", "/documents/*"] // Worker runs first here
}
}
Your Worker then does:
if (url.pathname.startsWith("/assets/protected/") || url.pathname.startsWith("/documents/")) {
const user = await authenticate(request, env);
if (!user) return Response.redirect("/login", 302);
return env.ASSETS.fetch(request); // serve gated file
}
return env.ASSETS.fetch(request); // public: let assets handle it
This pattern is officially documented as “run Worker first for selective paths.” (Cloudflare Docs)
Make the Worker run first for everything (optional)
If you want all requests to be Worker-first (useful for logging/i18n/global auth), set:
"assets": {
"directory": "./public",
"binding": "ASSETS",
"run_worker_first": true
}
Note: all matching requests will now invoke your Worker (impacts billing/limits). (Cloudflare Docs)
Nice combos / tips
-
SPA routing: If you’re building an SPA, pair this with
"not_found_handling": "single-page-application"
so “pretty URLs” work. You can still specifyrun_worker_first
as an array. (Cloudflare Docs) -
Custom headers: Headers from a
_headers
file don’t apply to Worker-generated responses; set them in code when you gate & serve protected assets. (Cloudflare Docs) -
Negative patterns: You can exclude subpaths with
!/path/*
when using the array form (e.g., run Worker first for/api/*
except/api/docs/*
). (Cloudflare Docs)
🧩 1. SPA routing ("not_found_handling": "single-page-application"
)
If you’re hosting a Single Page Application (React, Vue, Svelte, etc.), the browser might request “pretty URLs” like /dashboard
or /settings/profile
, but those don’t correspond to real files on disk.
Setting
"not_found_handling": "single-page-application"
in your wrangler.toml
(or wrangler.jsonc
) tells Cloudflare:
“If an asset isn’t found, serve my
index.html
instead.”
That way, your SPA’s client-side router can handle navigation without 404s.
You can still combine this with "run_worker_first": [...]
so your Worker intercepts certain paths before the SPA fallback runs.
🧾 2. Custom headers (_headers
file limitation)
If you put a _headers
file in your public/
folder, it sets HTTP headers for static assets served directly from the asset system.
But when your Worker generates or modifies a response (e.g., after checking auth), those _headers
rules don’t apply — because the response is created by code, not the static server.
👉 So if you want things like Cache-Control
, Content-Security-Policy
, or custom CORS headers on gated/protected files, you must set them yourself in the Worker:
const resp = await env.ASSETS.fetch(request);
const headers = new Headers(resp.headers);
headers.set("Cache-Control", "private, no-store");
return new Response(resp.body, { headers });
🚫 3. Negative patterns in run_worker_first
When you configure "run_worker_first"
as an array, you can use a !
prefix to exclude certain subpaths.
This lets you fine-tune which routes trigger your Worker first.
Example:
"run_worker_first": ["/api/*", "!/api/docs/*"]
That means:
- The Worker runs first for everything under
/api/*
-
Except for
/api/docs/*
, which should be served directly as static assets
It’s handy for excluding docs or health-check routes from your auth or logic layer.
✅ In short:
-
not_found_handling
→ SPA fallback toindex.html
-
_headers
only affect static files, not Worker responses -
!
inrun_worker_first
→ exclude specific subpaths
TL;DR
Use assets.run_worker_first
to ensure your auth code runs before static assets are served—globally (true
) or just for protected prefixes (array). Then call env.ASSETS.fetch(request)
only after the user passes your checks. (Cloudflare Docs)
Top comments (0)