Firebase函数线上报Error 9 FAILED_PRECONDITION,模拟器正常
Firebase Cloud Function线上部署后出现FAILED_PRECONDITION错误
问题背景
本地Firebase模拟器中运行正常的Cloud Function,部署到线上后抛出Error 9 FAILED_PRECONDITION错误。尽管错误信息未明确提及索引,但推测可能与索引配置或代码逻辑异常相关。
错误日志
"Unhandled error Error: 9 FAILED_PRECONDITION: at callErrorFromStatus (/workspace/node_modules/@grpc/grpc-js/build/src/call.js:31:19) at Object.onReceiveStatus (/workspace/node_modules/@grpc/grpc-js/build/src/client.js:357:73) at Object.onReceiveStatus (/workspace/node_modules/@grpc/grpc-js/build/src/client-interceptors.js:323:181) at /workspace/node_modules/@grpc/grpc-js/build/src/resolving-call.js:94:78 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) for call at at ServiceClientImpl.makeServerStreamRequest (/workspace/node_modules/@grpc/grpc-js/build/src/client.js:340:32) at ServiceClientImpl.<anonymous> (/workspace/node_modules/@grpc/grpc-js/build/src/make-client.js:105:19) at /workspace/node_modules/@google-cloud/firestore/build/src/v1/firestore_client.js:227:29 at /workspace/node_modules/google-gax/build/src/streamingCalls/streamingApiCaller.js:38:28 at /workspace/node_modules/google-gax/build/src/normalCalls/timeout.js:44:16 at Object.request (/workspace/node_modules/google-gax/build/src/streamingCalls/streaming.js:130:40) at makeRequest (/workspace/node_modules/retry-request/index.js:141:28) at retryRequest (/workspace/node_modules/retry-request/index.js:109:5) at StreamProxy.setStream (/workspace/node_modules/google-gax/build/src/streamingCalls/streaming.js:121:37) at StreamingApiCaller.call (/workspace/node_modules/google-gax/build/src/streamingCalls/streamingApiCaller.js:54:16) Caused by: Error at Query._get (/workspace/node_modules/@google-cloud/firestore/build/src/reference.js:1738:23) at Query.get (/workspace/node_modules/@google-cloud/firestore/build/src/reference.js:1726:21) at /workspace/timesheet/management.js:219:28 at /workspace/node_modules/firebase-functions/lib/common/onInit.js:33:16 at fixedLen (/workspace/node_modules/firebase-functions/lib/v1/providers/https.js:77:47) at /workspace/node_modules/firebase-functions/lib/common/providers/https.js:458:32 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) { code: 9, details: '', metadata: Metadata { internalRepr: Map(1) { 'x-debug-tracking-id' => [Array] }, options: {} } }"
问题代码
const filterTimesheetWorklogs = (worklogs, uid) => { return worklogs.filter(worklog => worklog.project.timesheetApprovers.includes(uid)) } const isSpawnAllApproved = async (virtualTimesheetId, uid) => { // 所有拥有相同virtualTimesheetId且当前用户是审批人的工时表都已通过审批 const collectionGroup = admin.firestore().collectionGroup("timesheets") const query = collectionGroup .where("virtualTimesheetId", "==", virtualTimesheetId) const snap = await query.get() const authorizedDocs = snap.docs.filter(async doc => { // 之前尝试过通过父文档获取项目信息,没用 // const projectRef = doc.ref.parent.parent // const projectSnap = await projectRef.get() // const projectData = projectSnap.data() const { projectId } = doc.data() const projectCollectionGroup = admin.firestore().collectionGroup("projects") const projectQuery = projectCollectionGroup.where("timesheetApprovers", "array-contains", uid) const projectSnap = await projectQuery.get() const projectData = projectSnap.docs .map(doc => ({ id: doc.id, ...doc.data() })) .find(project => project.id === projectId) return projectData.timesheetApprovers.includes(uid) }) return authorizedDocs.every(doc => { const data = doc.data() return data.status === "approved" }) } const getAuthorizedVirtualTimesheets = functions.https.onCall(async (data, context) => { const { filter: { start, end }, companyId } = data; const { uid, token: { roles = {} } } = context.auth if ([ Object.keys(roles).includes(companyId), roles[companyId].includes("timesheet approver") || roles[companyId].includes("admin"), ].some(v => !v)) { throw new functions.https.HttpsError("permission-denied", "Unauthorized access."); } const collectionGroup = admin.firestore().collectionGroup("virtualTimesheets") const query = collectionGroup // 试过移除这两个where条件,错误依然存在 .where("createdAt", ">=", dayjs(start).startOf("day").toDate()) .where("createdAt", "<=", dayjs(end).endOf("day").toDate()) const snap = await query.get() // 试过把这段逻辑全删掉直接返回,错误消失 const docs = await Promise.all(snap.docs .map(async doc => ({ id: doc.id, ...doc.data(), worklogs: filterTimesheetWorklogs(doc.data().worklogs, uid), status: (await isSpawnAllApproved(doc.id, uid)) ? "approved" : null, }))) return docs })
已尝试的排查步骤
- 创建了以下复合索引,但无效:
- 集合ID:
virtualTimesheets - 字段:
createdAtASCcreatedAtDESC
- 范围:集合组
- 集合ID:
- 同时移除
where子句和Promise.all相关逻辑后,错误消失。
解决思路及方案
1. 修复异步过滤逻辑错误
isSpawnAllApproved函数中,Array.filter不支持异步回调——异步函数返回的是Promise而非布尔值,导致authorizedDocs全是Promise对象,后续every判断完全失效,这是触发异常的核心原因之一。
修改后的isSpawnAllApproved函数:
const isSpawnAllApproved = async (virtualTimesheetId, uid) => { const collectionGroup = admin.firestore().collectionGroup("timesheets") const query = collectionGroup.where("virtualTimesheetId", "==", virtualTimesheetId) const snap = await query.get() // 用map+Promise.all处理异步逻辑,再过滤有效结果 const authorizedDocs = await Promise.all( snap.docs.map(async doc => { const { projectId } = doc.data() // 直接通过文档ID查询项目,替代低效的集合组查询 const projectDoc = await admin.firestore().collection("projects").doc(projectId).get() if (!projectDoc.exists) return false const projectData = projectDoc.data() // 验证用户权限后返回文档或false return projectData.timesheetApprovers.includes(uid) ? doc : false }) ) // 过滤无效项,检查所有有效文档是否已审批 return authorizedDocs.filter(Boolean).every(doc => doc.data().status === "approved") }
2. 配置正确的集合组索引
针对集合组查询,需创建对应索引:
- 针对
virtualTimesheets集合组的createdAt范围查询:创建集合组索引,字段为createdAt(ASC/DESC均可,范围查询仅需单字段索引),选择对应集合ID并勾选"集合组"选项。 - 针对
timesheets集合组的virtualTimesheetId相等查询:创建集合组索引,字段为virtualTimesheetId。
3. 优化查询性能
避免不必要的集合组查询,直接通过文档ID获取项目数据,减少索引依赖同时提升查询效率。
4. 确认服务账号权限
线上环境中,Cloud Function的服务账号(格式为PROJECT_ID@appspot.gserviceaccount.com)需拥有Firestore读取权限,可在Firebase控制台中为其授予Cloud Datastore User或Firebase Admin角色。
内容的提问来源于stack exchange,提问作者starleaf1
相关产品推荐
相关产品推荐

