你在 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 月,官方顺序是:
- 保留命名空间(
/__/*,Firebase SDK 自动配置和 auth 辅助用) - 精确匹配的静态内容(
public/ build 目录里的文件) - 配置的 redirects
- 配置的 rewrites(第一条匹配的规则生效)
- 自定义
404.html - 默认 404
所以同路径的静态文件会同时赢过 redirect 和你的 rewrite,redirect 又赢过 rewrite,而在 rewrites 内部只有第一条匹配的规则会执行。理解这条流水线,就能一次修到对的那一层。
先判断你属于哪种情况
| 现象 | 最可能的原因 | 跳到 |
|---|---|---|
/api/... 返回的是 SPA 的 HTML(你的 index.html) | ** rewrite 排在 /api/** 前面 | Step 1 |
/api 返回一个文件而不是函数 | 静态文件(public/api.html、api/index.html)把它盖住了 | Step 2 |
函数在它自己的 *.run.app / 直连 URL 能用,走 Hosting 就 404 | 函数和 rewrite 的 region 不一致 | Step 3 |
rewrite 指向的函数名不在 functions:list 里 | 函数其实没部署成功 | Step 3 |
改了 firebase.json 但还是旧路由 | 你跑的是 --only functions,不是 --only hosting | Step 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、cleanUrls 或 trailingSlash——这些都在 Hosting release 里。在你跑 --only hosting(或全量 firebase deploy)之前,旧的 Hosting 配置会一直对外服务。
如何判断: 看你上一次 firebase deploy 命令带了什么 flag。
5. cleanUrls / trailingSlash 改写了路径
{
"cleanUrls": true,
"trailingSlash": false
}
cleanUrls: true 会去掉 .html 并把 /about.html 301 重定向到 /about。trailingSlash: 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 时函数也一起回滚。
如何确认修好了
curl -v https://your-app.web.app/api/test返回200且响应体是函数内容(不是 HTML)。- 深层 SPA 路由如
https://your-app.web.app/dashboard/123返回index.html(状态200,你的应用外壳),而不是 404。 firebase functions:list里函数名 + region 跟 rewrite 完全一致。- 硬刷新或用无痕窗口绕过浏览器缓存,再重跑一次 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、cleanUrls 或 trailingSlash 之后,要跑 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。