Firebase Hosting Rewrites Not Firing (2026)

Your firebase.json rewrites for an SPA or Cloud Function don't trigger. Fix the order, region, and deploy target in minutes.

You set rewrites in firebase.json/api/** to a Cloud Function and ** to your SPA’s index.html. After deploy, /api/xxx returns 404 instead of hitting the function, or a deep route like /dashboard/123 404s instead of serving index.html. The config looks right but nothing fires.

Fastest fix (covers ~70% of cases): put your specific rules first and the ** SPA fallback dead last, then redeploy hosting with firebase deploy --only hosting. A ** rewrite listed before /api/** swallows every request, and --only functions never republishes your rewrite rules.

The reason this is confusing is that Firebase Hosting has a fixed precedence pipeline you can’t see in the dashboard. As of June 2026 the official order is:

  1. Reserved namespace (/__/*, used by the Firebase SDK auto-config and auth helpers)
  2. Exact-match static content (files in your public / build dir)
  3. Configured redirects
  4. Configured rewrites (first matching rule wins)
  5. Custom 404.html
  6. Default 404

So a static file at the same path beats both a redirect and your rewrite, a redirect beats a rewrite, and within rewrites only the first matching rule runs. Once you internalize that pipeline, you fix the right layer the first time.

Which bucket are you in?

SymptomMost likely causeJump to
/api/... returns SPA HTML (your index.html)** rewrite is ordered before /api/**Step 1
/api serves a file, not the functionA static file (public/api.html, api/index.html) shadows itStep 2
Function works on its *.run.app / direct URL but 404s through HostingRegion mismatch between function and rewriteStep 3
Rewrite points to a function name that isn’t in functions:listFunction never actually deployedStep 3
Changed firebase.json but old routing still servesYou ran --only functions, not --only hostingStep 4
Paths broke after enabling cleanUrls / trailingSlashSource patterns no longer match the rewritten pathsee cause 5

Common causes

Ordered by hit rate, highest first.

1. Wrong rewrite order (first match wins)

{
  "rewrites": [
    { "source": "**", "destination": "/index.html" },
    { "source": "/api/**", "function": { "functionId": "api" } }
  ]
}

The ** rule above matches everything, so /api/** is never reached. Most specific first; the SPA catch-all is always last.

How to spot it: is a ** source listed before any narrower pattern in your rewrites array?

2. Same-path static file takes precedence

Exact-match static content sits above rewrites in the pipeline. If public/api.html (or api/index.html) exists, a request to /api gets the static file, and your function rewrite never runs.

How to spot it: ls -R public/ (or your build output dir) for files whose path collides with a rewrite source.

3. Function region doesn’t match the rewrite

{
  "rewrites": [
    { "source": "/api/**", "function": { "functionId": "api", "region": "us-east1" } }
  ]
}

If the function is actually deployed in us-central1 but the rewrite names us-east1, Hosting can’t find it and falls through to 404. When region is omitted, the CLI auto-detects it from your function source and defaults to us-central1. If the same function is deployed to multiple regions, the CLI requires you to specify region explicitly.

A related failure: the deploy command reported success but one function silently failed to build, so the rewrite points to a function that doesn’t exist.

How to spot it: firebase functions:list shows each function’s exact name and region. Compare both against the rewrite config.

4. Edited firebase.json but didn’t deploy hosting

firebase deploy --only functions does not republish rewrites, redirects, headers, cleanUrls, or trailingSlash. Those live in the Hosting release. The previous Hosting config keeps serving until you run --only hosting (or a full firebase deploy).

How to spot it: check the flags on your last firebase deploy command.

5. cleanUrls / trailingSlash rewrote the path

{
  "cleanUrls": true,
  "trailingSlash": false
}

cleanUrls: true drops .html and 301-redirects /about.html to /about. trailingSlash: false 301-removes trailing slashes (/about/ to /about). After enabling either, your rewrite source patterns must match the post-normalization path. Note: trailingSlash only affects static content — it does not apply to rewrites that proxy to Cloud Functions or Cloud Run.

How to spot it: cleanUrls / trailingSlash are on, but your rewrite sources still use the old .html or trailing-slash form.

Shortest path to fix

Step 1: Reorder — specific first

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "/api/**", "function": { "functionId": "api", "region": "us-east1" } },
      { "source": "/webhooks/**", "function": { "functionId": "webhookHandler" } },
      { "source": "**", "destination": "/index.html" }
    ]
  }
}

Rule: most specific to most general; the SPA fallback is always the last entry.

Step 2: Remove conflicting static files

ls -R public/
# If api.html, api/index.html, etc. exist, move them out of the deploy dir
mv public/api.html /tmp/
firebase deploy --only hosting

Step 3: Align the function region (and confirm it deployed)

# Function's actual name + region
firebase functions:list

Match that region in firebase.json:

{
  "source": "/api/**",
  "function": { "functionId": "api", "region": "asia-east1" }
}

Or pin the region in your function code and redeploy. If functions:list doesn’t show the function at all, it never deployed — fix the build error and run firebase deploy --only functions:api.

Step 4: Actually deploy the hosting config

# Republish rewrites only
firebase deploy --only hosting

# Changed both firebase.json and functions
firebase deploy --only hosting,functions

# Confirm the live release + its rewrites
firebase hosting:sites:list

firebase deploy --only hosting automatically clears the Firebase CDN cache for your static content, so updated rewrite rules take effect on the next request — no manual purge needed.

Step 5: Verify with the emulator

firebase emulators:start --only hosting,functions

# Visit http://localhost:5000/api/test — does it hit the function?

Works locally but fails in prod = a config or deploy issue (region, missing function, IAM). Fails locally too = a config bug in firebase.json. Note the emulator routes all rewrites to the default region, so it can’t catch a region mismatch — verify that against prod.

Step 6: curl the real response

curl -v https://your-app.web.app/api/test

Read the result like a decision tree:

  • 200 + function body → rewrite is working.
  • 404 + your HTML → no rewrite matched; request fell to 404.html or the SPA fallback (recheck Step 1 and 2).
  • 404 + JSON/empty from the function → rewrite matched but the function itself returned 404.
  • 500 → rewrite matched, function ran and threw (check function logs, not the rewrite).

Step 7: Combine redirects + rewrites for complex routing

{
  "redirects": [
    { "source": "/old-api/**", "destination": "/api/:1", "type": 301 }
  ],
  "rewrites": [
    { "source": "/api/**", "function": { "functionId": "api" } },
    { "source": "**", "destination": "/index.html" }
  ]
}

Redirects sit above rewrites in the pipeline, so the 301 fires before any rewrite is considered.

Step 8 (2nd-gen functions): pin the function to the hosting release

If you keep getting version skew — new front-end deployed but the function rewrite serves the old behavior, or a preview channel can’t reach the function — add pinTag to the rewrite. This is 2nd-gen-only.

{
  "source": "/api/**",
  "function": { "functionId": "api", "region": "us-central1", "pinTag": true }
}

A pinned function is deployed alongside your static resources even on firebase deploy --only hosting, works on preview channels, and rolls back together with the site if you revert a release.

How to confirm it’s fixed

  1. curl -v https://your-app.web.app/api/test returns 200 with the function’s body (not HTML).
  2. A deep SPA route like https://your-app.web.app/dashboard/123 serves index.html (status 200, your app shell), not a 404.
  3. firebase functions:list shows the function name + region matching the rewrite exactly.
  4. Hard-reload or test in a private window to bypass any browser cache, then re-run the curl.

Prevention

  • Document why the rewrite order is what it is (use a JSONC config or a comment in your deploy README); reorder reviews catch the **-too-early bug.
  • Add a CI post-deploy step that curls every critical rewrite path and fails the build on a wrong status.
  • Don’t put files like api.html / webhooks.html in your deploy dir — they shadow rewrites of the same name.
  • Standardize on one region; deploy all functions there so no per-rewrite region is needed.
  • Route firebase.json changes through PR review so an accidental reorder is visible in the diff.
  • Run a full routing pass in the emulator before deploy, then re-test against the production URL — the emulator can’t reproduce region or IAM problems.

FAQ

Q: Why is my SPA rewrite swallowing the API path? A: Firebase Hosting matches rewrites top-down, first match wins. A ** source listed before /api/** catches every request and never reaches the API rule. Order from most specific to most general; the ** SPA fallback is always last.

Q: How do I tell whether a 404 came from the rewrite missing or from the function erroring? A: curl -v the URL. A 404 with an HTML body = the rewrite didn’t match and the request fell to 404.html or the SPA fallback. A 404 with a JSON/empty body from the function = the rewrite matched but the function returned 404. A 500 = the function ran and threw. Each needs a different fix.

Q: Do I need to redeploy hosting after every firebase.json change? A: Yes. firebase deploy --only functions does not republish hosting config. After editing rewrites, redirects, headers, cleanUrls, or trailingSlash, run firebase deploy --only hosting (or a full firebase deploy). The previous hosting config keeps serving until then.

Q: Does a static file really beat my rewrite? A: Yes. As of June 2026 the precedence is reserved namespace → exact-match static content → redirects → rewrites → 404. A file at the same path as a rewrite source is served before the rewrite ever runs, so remove or rename conflicting files in your deploy dir.

Q: Does the emulator catch all rewrite bugs? A: It catches config bugs — wrong order, wrong source pattern, missing function — but misses environment-specific ones: a function deployed in a non-default region (the emulator routes everything to the default region), missing IAM on the production function, or a stale CDN/browser cache. Always test the production URL after deploy.

Q: After deploy, a fresh load still serves the old behavior. Why? A: firebase deploy --only hosting clears the Firebase CDN cache automatically, so static content and rewrite rules flip on the next request. Lingering staleness is usually your browser cache (hard-reload or use a private window) or a Cloud Function/Cloud Run response cached per its own Cache-Control header — those aren’t purged by a hosting deploy. For a safe pre-check, deploy to a preview channel with firebase hosting:channel:deploy preview1 and validate before promoting.

Tags: #Backend #Debug #Troubleshooting #Firebase