DEV Community

teaganga
teaganga

Posted on

How run_worker_first, SPA Routing, and _headers Work in Cloudflare Workers

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 from env.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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 specify run_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"
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

🚫 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/*"]
Enter fullscreen mode Exit fullscreen mode

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 to index.html
  • _headers only affect static files, not Worker responses
  • ! in run_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)