【问题标题】:Nested HTTP requests in Firebase cloud functionFirebase 云函数中的嵌套 HTTP 请求
【发布时间】:2019-03-22 11:35:54
【问题描述】:

我正在使用 HTTP 触发的 Firebase 云函数来发出 HTTP 请求。我取回了一系列结果(来自 Meetup.com 的事件),并将每个结果推送到 Firebase 实时数据库。但是对于每个结果,我还需要对一条额外的信息(托管事件的组的类别)发出另一个 HTTP 请求,以折叠到我为该事件推送到数据库的数据中。这些嵌套请求会导致云函数崩溃,并出现我无法理解的错误。

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const request = require('request');

exports.foo = functions.https.onRequest(
    (req, res) => {
        var ref = admin.database().ref("/foo");
        var options = {
            url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
            json: true
        };
        return request(
            options,
            (error, response, body) => {
                if (error) {
                    console.log(JSON.stringify(error));
                    return res.status(500).end();
                }
                if ("results" in body) {
                    for (var i = 0; i < body.results.length; i++) {
                        var result = body.results[i];
                        if ("name" in result &&
                            "description" in result &&
                            "group" in result &&
                            "urlname" in result.group
                        ) {
                            var groupOptions = {
                                url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
                                json: true
                            };
                            var categoryResult = request(
                                groupOptions,
                                (groupError, groupResponse, groupBody) => {
                                    if (groupError) {
                                        console.log(JSON.stringify(error));
                                        return null;
                                    }
                                    if ("category" in groupBody &&
                                        "name" in groupBody.category
                                    ) {
                                        return groupBody.category.name;
                                    }
                                    return null;
                                }
                            );
                            if (categoryResult) {
                                var event = {
                                    name: result.name,
                                    description: result.description,
                                    category: categoryResult
                                };
                                ref.push(event);
                            }
                        }
                    }
                    return res.status(200).send("processed events");
                } else {
                    return res.status(500).end();
                }
            }
        );
    }
);

函数崩溃,日志显示:

Error: Reference.push failed: first argument contains a function in property 'foo.category.domain._events.error' with contents = function (err) {
      if (functionExecutionFinished) {
        logDebug('Ignoring exception from a finished function');
      } else {
        functionExecutionFinished = true;
        logAndSendError(err, res);
      }
    }
    at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1436:15)
    at /user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1479:13
    at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/@firebase/util/dist/index.node.cjs.js:837:13)
    at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1462:14)
    at /user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1479:13
    at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/@firebase/util/dist/index.node.cjs.js:837:13)
    at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1462:14)
    at /user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1479:13
    at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/@firebase/util/dist/index.node.cjs.js:837:13)
    at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/@firebase/database/dist/index.node.cjs.js:1462:14)

如果我省略了获取组类别的部分,其余代码工作正常(只需将每个事件的名称和描述写入数据库,没有嵌套请求)。那么正确的做法是什么?

【问题讨论】:

  • 您将通过使用承诺链而不是深度嵌套回调(如今这是一种 JavaScript 反模式)来清理大量此类代码(并可能揭示错误的根源)。忽略ref.push(event) 返回的承诺还有一个潜在的问题。

标签: javascript node.js firebase firebase-realtime-database google-cloud-functions


【解决方案1】:

我怀疑这个问题是由于回调造成的。当您使用 firebase 函数时,导出的函数应该等待一切执行或返回一个承诺,一旦一切完成执行,该承诺就会解决。在这种情况下,导出的函数将在其余执行完成之前返回。

这是一个基于更多承诺的开始 -

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const request = require("request-promise-native");

    exports.foo = functions.https.onRequest(async (req, res) => {
    const ref = admin.database().ref("/foo");
    try {
        const reqEventOptions = {
            url:
                "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=xxxxxx",
            json: true
        };
        const bodyEventRequest = await request(reqEventOptions);
        if (!bodyEventRequest.results) {
            return res.status(200).end();
        }
        await Promise.all(
            bodyEventRequest.results.map(async result => {
                if (
                    result.name &&
                    result.description &&
                    result.group &&
                    result.group.urlname
                ) {
                    const event = {
                        name: result.name,
                        description: result.description
                    };

                    // get group information
                    const groupOptions = {
                        url:
                            "https://api.meetup.com/" +
                            result.group.urlname +
                            "?sign=true&photo-host=public&key=xxxxxx",
                        json: true
                    };

                    const categoryResultResponse = await request(groupOptions);
                    if (
                        categoryResultResponse.category &&
                        categoryResultResponse.category.name
                    ) {
                        event.category = categoryResultResponse.category.name;
                    }

                    // save to the databse
                    return ref.push(event);
                }
            })
        );
        return res.status(200).send("processed events");
    } catch (error) {
        console.error(error.message);
    }
});

更改概览 -

  • 使用 await 和 async 调用来等待事情完成而不是在回调中触发(async 和 await 通常比使用 .then 函数的 Promise 更容易阅读,因为执行顺序是代码的顺序)李>
  • 使用的 request-promise-native 支持 Promise / await(即 await 意味着等到 Promise 返回,所以我们需要返回 Promise 的东西)
  • 对变量使用 const 和 let 与 var;这改善了变量的范围
  • 不要像 if(is good) { do good things } 那样做检查,而是使用 if(isbad) { return some error} do good thin。这使代码更易于阅读,并防止出现大量不知道它们在哪里结束的嵌套 if
  • 使用 Promise.all() 以便并行检索每个事件的类别

【讨论】:

  • 这是否需要告诉 Firebase 使用 Node.js 8?
  • 是的,这个是基于 Node.js 8 的,所以你需要在你的 package.json 中添加 "engines": { "node": "8" }。
  • 以状态码 500 结束,并且有一个日志条目仅包含消息“{}”。我不知道从那里去哪里。
  • 云函数日志中的任何内容以及消息的详细信息?此外,可能最好使用 console.error() 而不是我原来的日志。
  • 只有调试级条目表示它开始并以状态码 500 结束,而在这之间有一个只显示“{}”的信息级条目。
【解决方案2】:

您应该在代码中实现两个主要更改:

  • 由于request 不返回承诺,您需要为request 使用接口包装器,例如request-promise,以便正确链接不同的异步事件(请参阅Doug 对您的问题的评论)
  • 由于您随后将使用request-promise 多次(并行)调用不同的端点,您需要使用Promise.all() 以等待所有承诺解决后再发回响应。对 Firebase push() 方法的不同调用也是如此。

因此,按照以下几行修改您的代码应该可以工作。

我让你修改它,让你得到namedescription 的值,用于构造event 对象。 results 数组中项的顺序与promises 中的一项完全相同。因此,知道这一点,您应该能够在 results.forEach(groupBody =&gt; {}) 中获取 namedescription 的值,例如通过将这些值保存在全局数组中。


const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

var rp = require('request-promise');

exports.foo = functions.https.onRequest((req, res) => {
  var ref = admin.database().ref('/foo');
  var options = {
    url:
      'https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****',
    json: true
  };
  rp(options)
    .then(body => {
      if ('results' in body) {
        const promises = [];
        for (var i = 0; i < body.results.length; i++) {
          var result = body.results[i];
          if (
            'name' in result &&
            'description' in result &&
            'group' in result &&
            'urlname' in result.group
          ) {
            var groupOptions = {
              url:
                'https://api.meetup.com/' +
                result.group.urlname +
                '?sign=true&photo-host=public&key=****',
              json: true
            };

            promises.push(rp(groupOptions));
          }
        }
        return Promise.all(promises);
      } else {
        throw new Error('err xxxx');
      }
    })
    .then(results => {
      const promises = [];

      results.forEach(groupBody => {
        if ('category' in groupBody && 'name' in groupBody.category) {
          var event = {
            name: '....',
            description: '...',
            category: groupBody.category.name
          };
          promises.push(ref.push(event));
        } else {
          throw new Error('err xxxx');
        }
      });
      return Promise.all(promises);
    })
    .then(() => {
      res.send('processed events');
    })
    .catch(error => {
      res.status(500).send(error);
    });
});

【讨论】:

  • 我得到了它的工作,基于对 R. Wright 的 answer 的一些更改。请查看我的answer 中的代码。与您的方法相比,我采取的方法有什么不妥之处吗?
【解决方案3】:

我做了一些更改,让它与 Node 8 一起工作。我将它添加到我的package.json

"engines": {
    "node": "8"
}

这就是代码现在的样子,基于 R. Wright 的 answer 和一些 Firebase 云函数示例代码。

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const request = require("request-promise-native");

exports.foo = functions.https.onRequest(
    async (req, res) => {
        var ref = admin.database().ref("/foo");
        var options = {
            url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
            json: true
        };
        await request(
            options,
            async (error, response, body) => {
                if (error) {
                    console.error(JSON.stringify(error));
                    res.status(500).end();
                } else if ("results" in body) {
                    for (var i = 0; i < body.results.length; i++) {
                        var result = body.results[i];
                        if ("name" in result &&
                            "description" in result &&
                            "group" in result &&
                            "urlname" in result.group
                        ) {
                            var groupOptions = {
                                url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
                                json: true
                            };
                            var groupBody = await request(groupOptions);
                            if ("category" in groupBody && "name" in groupBody.category) {
                                var event = {
                                    name: result.name,
                                    description: result.description,
                                    category: groupBody.category.name
                                };
                                await ref.push(event);
                            }
                        }
                    }
                    res.status(200).send("processed events");
                }
            }
        );
    }
);

【讨论】:

    最近更新 更多