你部署了一个 Firebase Cloud Function,从前端调用:
const result = await httpsCallable(functions, 'sendEmail')({ to: 'x' });
报错通常是下面之一:
FirebaseError: Function not found: sendEmail
FirebaseError: internal
或者直接 HTTP fetch 返回:
404 Not Found
但 Firebase Console 里明明能看到 Functions 列表里有 sendEmail。这是 Cloud Functions 最让人困惑的错:console 里”看得见”不等于这个客户端”调得到”。原因几乎永远是三者之一——region、名字、部署状态。
最快修复(约 70% 命中): 函数实际运行的 region 跟客户端 SDK 指向的不是同一个。先跑 firebase functions:list 读出真实 region,再把这个 region 原样传给客户端的 getFunctions(app, 'us-east1')。其他情况见下文。
你属于哪一类?
先跑一条命令,再对症:
firebase functions:list --project my-project-prod
| 现象 | 最可能的原因 | 跳转 |
|---|---|---|
| 列表里有这个函数,但 region 跟客户端设的不一样 | Region 不匹配 | Step 2 |
| 列表里根本没有这个函数 | 部署被跳过 / build 失败 / 被 GC | Step 4 |
| 列表里的名字和你调用的字符串大小写/拼写不同 | 函数名不一致 | 原因 2 |
浏览器 console 报 CORS error、status 0 或 preflight 被拦 | 是 CORS,不是函数不存在 | 原因 6 |
curl 返回 200/401/403,但 SDK 调用失败 | v1/v2 路径或 auth 问题,不是 not found | Step 6 |
常见原因
按命中率从高到低。
1. Region 不匹配(客户端和函数)
最高频。函数声明了 region: 'asia-east1',但客户端 SDK 没指定 region。JS 客户端 SDK 在你调用 getFunctions(app)(不带 region 参数)时仍然默认连 us-central1——于是它去 us-central1 找函数,可函数不在那。
最近的 Firebase CLI 版本有一个变化经常坑人:CLI 不再无条件部署到 us-central1。它会尽量根据你的项目配置和触发数据源(Firestore、Storage)的 region 来推断部署位置。所以函数可能落在一个你从没显式选过的 region,而客户端还在硬默认 us-central1。永远以 functions:list 读到的 region 为准,别凭假设。
如何判断
- 函数代码:
onCall({ region: 'asia-east1' }, ...)(v2)或老语法functions.region('asia-east1').https.onCall(...)(v1) - 客户端:
getFunctions(app)不带第二个参数 = 落到us-central1 - v2 callable 的 region 不匹配经常表现为 CORS / preflight 错误或
FirebaseError: internal,而不是干净的 “not found”——因为 SDK 打到了一个不存在的 URL,preflight 拿不到有效响应。别被措辞误导。
2. 函数名拼写 / 大小写不一致
// 函数代码
export const sendEmail = onCall(...);
// 客户端
httpsCallable(functions, 'send-email') // 错:用了 kebab-case
httpsCallable(functions, 'sendmail') // 错:拼错
Firebase 函数名就是 export 的变量名——严格区分大小写,必须完全一致。
如何判断:从 firebase functions:list 或 console 复制确切函数名,跟客户端调用里的字符串逐字对照。
3. Build 静默失败,没真部署
CLI 打印了 Deploy complete!,但其中一个函数因为 TypeScript 错误或 missing dependency 被跳过了。其他函数好了,这个静默没上去。
如何判断:firebase deploy --only functions --debug,在输出里搜 Failed to upload、skipped,或者靠近顶部的 TypeScript 编译错误。
4. 用了 v1 / v2 SDK 混乱
Firebase Functions 分 v1(firebase-functions)和 v2(firebase-functions/v2)。两版部署到不同 endpoint,调用方式也微妙不同。v2 底层跑在 Cloud Run 上,这也是为什么 v2 函数会拿到两个 URL(详见 Step 6)。
如何判断:看 import——import { onCall } from 'firebase-functions/v2/https'(v2)vs import * as functions from 'firebase-functions'(v1)。
5. 函数被上一次部署删掉了(garbage collection)
如果上一次 deploy 没 export sendEmail,Firebase 会当它被移除并从项目里删掉。之后你再 export 回来,但 deploy 还没跑完(或失败了),函数就真的不在。
如何判断:firebase functions:list 看它现在到底存不存在。
6. CORS 让请求根本没离开浏览器
HTTPS callable trigger 只有在配置了 CORS 时才会自动处理 CORS preflight。v2 要在函数 options 里设 cors: true(或显式的来源白名单)。不设的话,前端跨域调用会被浏览器在客户端直接拦掉——看起来像 404,但其实从没到 server。
如何判断:浏览器 Network 标签里看到 status 0 或明确的 CORS policy 错误(例如:Access to fetch at 'https://us-central1-my-project.cloudfunctions.net/sendEmail' from origin 'https://my-project.web.app' has been blocked by CORS policy),而不是函数返回的干净 404。
最短修复路径
Step 1:先 list 确认存在
firebase functions:list --project my-project-prod
# 输出示例:
# ┌──────────────┬─────────┬───────────────┬─────────┐
# │ Function │ Version │ Trigger │ Region │
# ├──────────────┼─────────┼───────────────┼─────────┤
# │ sendEmail │ v2 │ https │ us-east1│
# └──────────────┴─────────┴───────────────┴─────────┘
这一条同时确认三件事:确切函数名、generation(v1/v2)、region。如果列表里没有 sendEmail,直接跳到 Step 4——你的问题是部署,不是 region。
Step 2:客户端 SDK 显式设 region
// v2 SDK
import { getFunctions, httpsCallable } from 'firebase/functions';
// 错:不带 region 参数 = 落到 us-central1
const fns = getFunctions(app);
// 对:跟 Step 1 里的 region 一致
const fns = getFunctions(app, 'us-east1');
const callSendEmail = httpsCallable(fns, 'sendEmail');
getFunctions 的第二个参数可以是 region(如 us-east1),也可以是托管 callable 的自定义域名。用 Step 1 里看到的那个确切 region 字符串。
Step 3:函数代码里显式声明 region
// v2
import { onCall } from 'firebase-functions/v2/https';
export const sendEmail = onCall(
{ region: 'us-east1', cors: true }, // 显式 region + CORS
async (req) => { /* ... */ }
);
在代码里钉死 region,部署就不会悄悄换地方(见原因 1 里 CLI 自动选 region 的行为)。
Step 4:重新 deploy 并真正看 build 日志
firebase deploy --only functions:sendEmail --debug 2>&1 | tee deploy.log
# 搜那些藏在长输出里的失败信号
grep -i "error\|failed\|skipped\|not deployed" deploy.log
Deploy complete! 并不能证明某个具体函数上去了。CLI 确实会报 build 失败,但在长输出里一刷就过去了;--debug 配合 tee 让你能回头搜。
Step 5:用 local emulator 验证
firebase emulators:start --only functions
// 客户端改连 emulator 而不是生产环境
import { connectFunctionsEmulator } from 'firebase/functions';
if (location.hostname === 'localhost') {
connectFunctionsEmulator(fns, '127.0.0.1', 5001);
}
emulator 上能调通、生产上不行 = 部署或 region 问题;emulator 上也 not found = 代码问题。(如果你的运行时把 localhost 解析成 IPv6 而 emulator 只绑了 IPv4,用 127.0.0.1 而不是 localhost。)
Step 6:直接 curl 这个 endpoint
v2 函数会暴露两个 URL:一个确定性的 cloudfunctions.net URL,和一个带随机 hash 的 Cloud Run *.a.run.app URL。cloudfunctions.net 这个手动拼起来最简单:
# v2 onCall endpoint(确定性 URL)
curl -X POST https://us-east1-my-project.cloudfunctions.net/sendEmail \
-H "Content-Type: application/json" \
-d '{"data":{"to":"x"}}'
# 404 = 真的不存在,或 URL 里 region 错了
# 200 / 401 / 403 = 存在;你的问题是 auth/permission,不是 not found
callable 要求 POST body 包在 data key 里,就像上面这样。如果 curl 通了但 SDK 不通,说明函数没问题,问题回到客户端的 region 或函数名不一致。
Step 7:v1 / v2 混用检查
// v1 写法
import * as functions from 'firebase-functions';
export const sendEmail = functions.https.onCall((data, context) => { /* ... */ });
// v2 写法
import { onCall } from 'firebase-functions/v2/https';
export const sendEmail = onCall((req) => { /* ... */ });
两个 generation 解析到不同的 URL 路径,混用就会调错地方。整个仓库统一用一个 generation。新函数都从 v2 起步,老的 v1 等有空再迁。
怎么确认修好了
firebase functions:list显示sendEmail在预期的 region 和 generation 上。- 客户端的
getFunctions(app, '<region>')里是同一个 region 字符串。 - 确定性 URL 的
curl返回200(或401/403,同样证明它存在)。 - 前端调用不再出现
Function not found、internal或 Network 标签里的 CORS error。
四条都过,not found 就是真的没了——剩下的是 auth 或业务逻辑,不再是路由问题。
预防建议
- 一个项目只用一个 region。写进 README,新函数都用它。
- 客户端 SDK 初始化时一定显式传 region,不要靠默认值。
- 抽成常量:
export const FUNCTIONS_REGION = 'us-east1';,client 和 server 都引用。 - deploy 后跑一个 health check,依次
curl所有 endpoint,非 2xx/4xx 就让 CI 失败。 - CI 加 typecheck 步骤,让 TypeScript 错误没法混进一次”成功”的部署。
- 函数名用 camelCase 并跟 JS export 一致,别混 kebab-case。
- v1 迁 v2 时全量一次改完,别一半 v1 一半 v2。
- 每次升级 CLI 后重新核对部署的 region——最近几个版本改了默认 region 的行为。
常见问题
为什么 console 里看得到函数,客户端却找不到?
console 列出的是所有 region 里部署的全部函数。客户端 SDK 只在你配置的那一个 region(或默认 us-central1)里找。console 里看得见、但对客户端是错的 region = not found。
我没设过 region,函数到底部署到哪了?
较新的 Firebase CLI 会根据项目配置和触发源推断 region,而不是一律用 us-central1。跑 firebase functions:list 看它实际落在哪,并在代码里钉死 region,让它别再乱跑。
我的报错是 internal 或 CORS error,不是 “Function not found”,是同一个问题吗?
常常是。指向错误 region 的 v2 callable 会打到一个不存在的 URL,失败的 preflight 表现为 CORS error 或 FirebaseError: internal,而不是干净的 not found。先修 region,再去追 CORS。
curl 返回 200,但 SDK 还是失败,为什么?
说明函数存在且可达,不是部署或路由问题。重新核对传给 getFunctions 的 region,以及 httpsCallable 里那个确切的名字字符串——客户端就剩这两个开关了。
callable 函数需要设 CORS 吗?
v2 onCall 需要在函数 options 里设 cors: true(或显式来源列表),这样 SDK 的 preflight 才能从你的网页来源通过。v1 callable 是自动处理的;v2 要你显式声明。
相关阅读
外部参考:Cloud Functions locations · Call functions from your app