Firebase Hosting Rewrites Not Firing (2026)

You configured rewrites for SPA / functions but they don't trigger.

You configured 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 deep SPA routes like /dashboard/123 404 instead of serving index.html. The config looks right, but it just doesn’t fire.

Firebase Hosting has a few invisible precedence rules: static files > first matching rewrite > 404.html. Once you internalize the precedence, fixing the right thing is straightforward.

Common causes

Ordered by hit rate, highest first.

1. Wrong rewrite order (first match wins)

{
  "rewrites": [
    { "source": "**", "destination": "/index.html" },  // ❌ swallows everything
    { "source": "/api/**", "function": "api" }         // never reached
  ]
}

Most specific first.

How to spot it: ** wildcard before /api/**?

2. Same-path static file takes precedence

If public/api.html exists, requests to /api get the static file, not the rewrite.

How to spot it: ls public/ for files that conflict with rewrite sources.

3. Function region doesn’t match rewrite region

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

Function actually deployed in us-central1, rewrite looks in us-east1 → 404.

How to spot it: firebase functions:list shows function region; compare to the rewrite config.

4. Function never actually deployed

Deploy command succeeds, but a single function silently failed to build (similar to firebase-deploy-permission-denied silent fail). Rewrite points to a non-existent function.

How to spot it: firebase functions:list — is the name listed?

5. cleanUrls / trailingSlash changed the path

{
  "cleanUrls": true,      // /about.html → /about
  "trailingSlash": false  // /about/ → /about
}

After enabling these, your source patterns must reflect the new paths.

How to spot it: cleanUrls / trailingSlash enabled but rewrite sources use old-style paths.

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

firebase deploy --only functions doesn’t update rewrites. Need --only hosting (or full deploy).

How to spot it: What flag did your last firebase deploy use?

Shortest path to fix

Step 1: Reorder — specific first

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

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

Step 2: Remove conflicting static files

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

Step 3: Align function region

# 1. Function's actual region
firebase functions:list

# 2. Match it in firebase.json
{
  "source": "/api/**",
  "function": {
    "functionId": "api",
    "region": "asia-east1"
  }
}

Or change the function’s region in code and redeploy.

Step 4: Confirm hosting config was deployed

firebase deploy --only hosting

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

# See what was deployed
firebase hosting:channel:list

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 = config / deploy issue in prod. Fails locally = config bug.

Step 6: curl to see the real response

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

# 200 + function body = rewrite working
# 404 + HTML = no rewrite, fell into SPA fallback
# 404 + empty = path doesn't exist anywhere

Step 7: Combine redirects + rewrites for complex cases

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

Redirects run first.

Prevention

  • Annotate firebase.json (use jsonc or an external doc); document why the rewrite order is what it is
  • CI post-deploy step: curl every critical rewrite path
  • Don’t put files like api.html / webhooks.html in public/ — too easy to conflict
  • One region per project; all functions go there; no per-rewrite region needed
  • firebase.json changes go through PR review — avoid accidental reordering
  • Default firebase deploy deploys everything; only use --only when you know what changed
  • Run a full routing test in the emulator before deploying
  • After deploy, test against the production URL — don’t trust emulator alone

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 rules 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 HTML body = rewrite didn’t match, request fell to 404.html or the SPA fallback. A 404 with JSON / empty body from the function = rewrite matched but the function returned 404. A 500 = function ran but threw. Different fixes for each case.

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 full firebase deploy). The previous hosting config keeps serving until then.

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 — function deployed in a different region, missing IAM on the production function, CDN cache serving a stale rewrite. Always test the production URL after deploy too.

Q: Why does a fresh deploy still serve the old rewrite for a few minutes? A: Firebase Hosting’s CDN caches firebase.json decisions at the edge for up to a few minutes. Hard-reload or test in incognito; if you need an immediate flip, run firebase hosting:channel:deploy to a preview channel first and validate before promoting.

Tags: #Backend #Debug #Troubleshooting #Firebase