Firebase Hosting rewrite 不生效

2026 最新:firebase.json 里 SPA / 函数的 rewrite 没触发——几分钟内修好顺序、region 和部署目标。

你在 firebase.json 里配了 rewrites——/api/** 转到 Cloud Function、** 转到 SPA 的 index.html。部署后访问 /api/xxx 直接 404 而不是走函数;或者深层路由 /dashboard/123 返回 404 而不是 index.html。配置看起来没问题,但就是不生效。

最快修复(覆盖约 70% 的情况): 把具体规则放最前面、** 这条 SPA fallback 放最后,然后用 firebase deploy --only hosting 重新部署 hosting。** 排在 /api/** 前面会吃掉所有请求;而 --only functions 根本不会重新发布你的 rewrite 规则。

之所以让人困惑,是因为 Firebase Hosting 有一条你在控制台里看不见的固定优先级流水线。截至 2026 年 6 月,官方顺序是:

  1. 保留命名空间(/__/*,Firebase SDK 自动配置和 auth 辅助用)
  2. 精确匹配的静态内容public / build 目录里的文件)
  3. 配置的 redirects
  4. 配置的 rewrites(第一条匹配的规则生效)
  5. 自定义 404.html
  6. 默认 404

所以同路径的静态文件会同时赢过 redirect 和你的 rewrite,redirect 又赢过 rewrite,而在 rewrites 内部只有第一条匹配的规则会执行。理解这条流水线,就能一次修到对的那一层。

先判断你属于哪种情况

现象最可能的原因跳到
/api/... 返回的是 SPA 的 HTML(你的 index.html** rewrite 排在 /api/** 前面Step 1
/api 返回一个文件而不是函数静态文件(public/api.htmlapi/index.html)把它盖住了Step 2
函数在它自己的 *.run.app / 直连 URL 能用,走 Hosting 就 404函数和 rewrite 的 region 不一致Step 3
rewrite 指向的函数名不在 functions:list函数其实没部署成功Step 3
改了 firebase.json 但还是旧路由你跑的是 --only functions,不是 --only hostingStep 4
开了 cleanUrls / trailingSlash 后路径就坏了source pattern 不再匹配改写后的路径见原因 5

常见原因

按命中率从高到低排列。

1. rewrites 顺序错(先匹配先生效)

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

上面的 ** 匹配一切,所以 /api/** 永远到不了。最具体的放前面,SPA 的 catch-all 永远放最后。

如何判断: rewrites 数组里有没有 ** 排在更窄的 pattern 前面?

2. 同路径静态文件优先

精确匹配的静态内容在流水线里排在 rewrites 上面。如果 public/api.html(或 api/index.html)存在,访问 /api 会拿到静态文件,你的函数 rewrite 根本不会跑。

如何判断: ls -R public/(或你的 build 输出目录),看有没有文件路径跟某个 rewrite source 撞上。

3. 函数 region 跟 rewrite 不一致

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

如果函数实际部署在 us-central1,rewrite 却写 us-east1,Hosting 找不到它,于是落到 404。region 省略时,CLI 会从函数源码里自动探测,默认是 us-central1。如果同一个函数部署到了多个 region,CLI 会要求你显式写出 region

一个相关的坑:deploy 命令报告成功,但某个函数 build 静默失败了,于是 rewrite 指向了一个不存在的函数。

如何判断: firebase functions:list 会列出每个函数的确切名字和 region,跟 rewrite 配置逐项对照。

4. 改了 firebase.json 但没 deploy hosting

firebase deploy --only functions 不会重新发布 rewrites、redirects、headers、cleanUrlstrailingSlash——这些都在 Hosting release 里。在你跑 --only hosting(或全量 firebase deploy)之前,旧的 Hosting 配置会一直对外服务。

如何判断: 看你上一次 firebase deploy 命令带了什么 flag。

5. cleanUrls / trailingSlash 改写了路径

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

cleanUrls: true 会去掉 .html 并把 /about.html 301 重定向到 /abouttrailingSlash: false 会 301 去掉末尾斜杠(/about//about)。开启任一项之后,你的 rewrite source pattern 必须匹配归一化之后的路径。注意:trailingSlash 只影响静态内容,不影响代理到 Cloud Functions 或 Cloud Run 的 rewrite。

如何判断: cleanUrls / trailingSlash 已开启,但 rewrite source 还在用旧的 .html 或带斜杠写法。

最短修复路径

Step 1:重排序——具体的放前面

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

规则:从最具体到最通用,SPA fallback 永远是最后一条。

Step 2:删掉冲突的静态文件

ls -R public/
# 如果有 api.html、api/index.html 等,从部署目录里移走
mv public/api.html /tmp/
firebase deploy --only hosting

Step 3:对齐函数 region(并确认它真的部署了)

# 函数的实际名字 + region
firebase functions:list

firebase.json 里把 region 对齐:

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

或者在函数代码里固定 region 后重新部署。如果 functions:list 里压根没有这个函数,说明它没部署成功——先修 build 错误,再跑 firebase deploy --only functions:api

Step 4:真正把 hosting 配置部署上去

# 只重新发布 rewrites
firebase deploy --only hosting

# firebase.json 和函数都改了
firebase deploy --only hosting,functions

# 确认线上 release 及其 rewrites
firebase hosting:sites:list

firebase deploy --only hosting 会自动清空 Firebase CDN 上的静态内容缓存,所以更新后的 rewrite 规则在下一次请求时就生效,不需要手动 purge。

Step 5:用 emulator 本地验证

firebase emulators:start --only hosting,functions

# 访问 http://localhost:5000/api/test —— 看是否走函数

本地正常但生产失败 = 配置或部署问题(region、函数没部署、IAM);本地也错 = firebase.json 本身有配置 bug。注意 emulator 会把所有 rewrite 都路由到默认 region,所以它抓不到 region 不一致这种问题——这一项要对着生产环境验证。

Step 6:用 curl 看真实响应

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

把结果当决策树读:

  • 200 + 函数响应体 → rewrite 正常工作。
  • 404 + 你的 HTML → 没有 rewrite 命中,请求落到了 404.html 或 SPA fallback(重查 Step 1 和 Step 2)。
  • 404 + 来自函数的 JSON/空响应 → rewrite 命中了,但函数自己返回了 404。
  • 500 → rewrite 命中、函数跑起来并抛了错(查函数日志,不是 rewrite 的问题)。

Step 7:复杂路由用 redirects + rewrites 组合

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

redirects 在流水线里排在 rewrites 上面,所以 301 会在任何 rewrite 被考虑之前先触发。

Step 8(2nd-gen 函数):把函数 pin 到 hosting release

如果你反复遇到版本错位——前端是新的,但函数 rewrite 还在跑旧逻辑,或者 preview channel 连不上函数——给 rewrite 加上 pinTag。这个只在 2nd-gen 可用。

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

被 pin 的函数即便在 firebase deploy --only hosting 时也会随静态资源一起部署,在 preview channel 上能用,并且当你回滚某个 release 时函数也一起回滚。

如何确认修好了

  1. curl -v https://your-app.web.app/api/test 返回 200 且响应体是函数内容(不是 HTML)。
  2. 深层 SPA 路由如 https://your-app.web.app/dashboard/123 返回 index.html(状态 200,你的应用外壳),而不是 404。
  3. firebase functions:list 里函数名 + region 跟 rewrite 完全一致。
  4. 硬刷新或用无痕窗口绕过浏览器缓存,再重跑一次 curl。

预防建议

  • 写清楚 rewrite 顺序为什么是这样(用 JSONC 配置或部署 README 里的注释);重排序时 review 就能抓到 ** 放太前面的 bug。
  • 在 CI 里加一步部署后检查:curl 每个关键 rewrite path,状态码不对就让 build 失败。
  • 别在部署目录里放 api.html / webhooks.html 这类文件——它们会盖住同名的 rewrite。
  • 统一用一个 region,所有函数都部署到那里,rewrite 就不必手写 region
  • firebase.json 改动走 PR review,意外的重排序在 diff 里能一眼看出来。
  • 部署前在 emulator 里跑一遍完整 routing,部署后再对着 production URL 测一遍——emulator 复现不了 region 和 IAM 问题。

常见问题

Q:为什么我的 SPA rewrite 把 API 路径吃掉了? A:Firebase Hosting 从上往下匹配 rewrite,先匹配先生效。** 排在 /api/** 前面会接走每个请求,永远到不了 API 规则。按从最具体到最通用排序,** SPA fallback 永远放最后。

Q:怎么区分 404 是 rewrite 没命中,还是函数报错? A:用 curl -v 看。404 带 HTML 响应体 = rewrite 没匹配,请求落到了 404.html 或 SPA fallback。404 带函数返回的 JSON/空响应 = rewrite 命中了但函数自己返回 404。500 = 函数跑起来并抛错。三种情况各有不同修法。

Q:每次改 firebase.json 都得重新部署 hosting 吗? A:是的。firebase deploy --only functions 不会重新发布 hosting 配置。改完 rewrites、redirects、headers、cleanUrlstrailingSlash 之后,要跑 firebase deploy --only hosting(或全量 firebase deploy)。在那之前旧的 hosting 配置会一直服务。

Q:静态文件真的会赢过我的 rewrite 吗? A:会。截至 2026 年 6 月,优先级是 保留命名空间 → 精确匹配静态内容 → redirects → rewrites → 404。和 rewrite source 同路径的文件会在 rewrite 跑起来之前先被返回,所以要删掉或重命名部署目录里的冲突文件。

Q:emulator 能抓到所有 rewrite bug 吗? A:它能抓配置类 bug——顺序错、source pattern 错、函数缺失——但抓不到环境相关的问题:函数部署在非默认 region(emulator 把一切都路由到默认 region)、生产函数缺 IAM、或者 CDN/浏览器缓存过期。部署后一定要对着 production URL 测。

Q:部署完,刷新还是旧行为,为什么? A:firebase deploy --only hosting 会自动清空 Firebase CDN 缓存,所以静态内容和 rewrite 规则在下一次请求就翻新。残留的”旧行为”通常是浏览器缓存(硬刷新或用无痕窗口),或者是按自身 Cache-Control 头缓存的 Cloud Function / Cloud Run 响应——这些不会被 hosting 部署清掉。想稳一点,可以先用 firebase hosting:channel:deploy preview1 部署到 preview channel 验证,再 promote。

相关阅读

标签: #后端 #排查 #排查 #Firebase