Firebase云函数返回RangeError:调用栈溢出问题求助
这个栈溢出错误的核心原因是Firebase在序列化你的函数返回值时遇到了循环引用。具体来说,你的代码返回的是once()方法生成的Promise,而这个Promise最终会解析为Firebase的DataSnapshot对象——这个对象内部存在循环引用(比如它的ref属性指向数据库引用,而数据库引用又关联着Snapshot相关的结构),导致lodash的mapValues在递归编码时陷入无限循环,最终触发栈溢出。
代码问题分析
你使用了once("value", lookupSnapshot => { ... })的回调形式,此时once()返回的Promise会直接解析为lookupSnapshot(也就是DataSnapshot对象),而你在回调里返回的update() Promise并没有被正确链式传递出去。换句话说,你的函数最终返回的是一个解析为DataSnapshot的Promise,而不是解析为update操作结果的Promise。Firebase尝试把这个DataSnapshot序列化为JSON时,就会因为循环引用引发栈溢出。
修复方案
1. 改用Promise链式调用替代回调参数
把once("value", 回调函数)的写法改成.once("value").then(回调函数),这样你在then里返回的update() Promise会成为整个Promise链的最终返回值,避免返回带有循环引用的DataSnapshot对象。
2. 确保返回可JSON序列化的值
update()方法返回的Promise会解析为一个空对象(或成功确认信息),这是可以被正常序列化的。
修改后的完整代码如下:
export const scrapeAssignments = functions.https.onCall((data, context) => { const htmlString = data.htmlString; if (!(typeof htmlString === 'string') || htmlString.length === 0) { throw new functions.https.HttpsError('invalid-argument', 'The function must be called with one argument "htmlString"'); } const userId = getUserIdFromCallableContext(context); console.log("userId", userId); let newAssignments: ScrapedAssignment[] = []; try { newAssignments = parseAssignment(htmlString); } catch (e) { const error = <Error>e; throw new functions.https.HttpsError('not-found', 'parsing error: ' + error.message); } // 改用.then()链式调用,替代回调参数 return admin.database().ref("lookup").child(userId).child("assignments") .once("value") .then(lookupSnapshot => { const oldAssignmentsLookup = lookupSnapshot.val() || {}; const newAssignmentsLookup = makeLookup(newAssignments); // 1. Create update values for scraped assignment data let scrapedAssignmentUpdateValues = newAssignments.reduce((prev, current) => { const prefixed = prefixObject(current.id + "/", current); return { ...prev, ...prefixed }; }, {}); // 2. Use the diff from the two lookups to find old assignments to delete const removeAssignmentsValues = {}; Object.keys(oldAssignmentsLookup).forEach(assignmentId => { if (isUndefined(newAssignmentsLookup[assignmentId])) { removeAssignmentsValues[assignmentId] = null; } }); // 3. Add other user values to newly found assignments Object.keys(newAssignmentsLookup).forEach(assignmentId => { if (isUndefined(oldAssignmentsLookup[assignmentId])) { const doneKey = assignmentId + "/done"; scrapedAssignmentUpdateValues[doneKey] = false; } }); const combinedValues = { ...scrapedAssignmentUpdateValues, ...removeAssignmentsValues }; // 返回update的Promise,链式传递 return admin.database().ref("userAssignments").child(userId).update(combinedValues); }) .catch(reason => { throw new functions.https.HttpsError('internal', 'Database reason: ' + reason); }); });
额外检查点
- 确认
combinedValues里的所有值都是普通JSON类型(字符串、数字、布尔、普通对象/数组),没有循环引用。300个键值对的数量完全在Firebase可调用函数的返回值限制内(单返回值最大10MB),所以数量不是问题。 - 检查
prefixObject和makeLookup函数是否生成了带有循环引用的对象,这也可能导致序列化时栈溢出。
内容的提问来源于stack exchange,提问作者Wiingaard

