res.status().send() 在 Promise.all 中无法正常工作

res.status().send() not working correctly in Promise.all

我正在编写一个代码来检查来自 promise.all 方法中两个不同 API 调用的授权,如果其中任何一个授权失败,将抛出相应的 res.send 方法作为错误,但我的控制台上显示 error : Cannot set headers after they are sent to the client 错误,我哪里出错了?

在屏幕上,显示 res.send 语句,但与此同时,此 error : Cannot set headers after they are sent to the client 错误显示在我的控制台上。我该如何解决这个问题?

我用两种不同的方式编写代码,但每次都显示相同的错误。

第一种方式(没有 .catch):

const isSubscribed = new Promise((resolve, reject) => {
  apiGet("/isSubscribed", token).then(async (Response) => {
    if (!isResStatusSubscribed(Response)) return res.status(401).send({ errorMessage: "Unauthorized Request." })
  })
})

const isAdmin = new Promise((resolve, reject) => {
  apiGet("/isAdmin", token).then(async (response) => {
    let isAdmin = response.data.Response.is_admin
    if (!isAdmin) return res.status(403).send({ errorMessage: "User is not an Admin" })
  })
})

Promise.all([isSubscribed, isAdmin]).then(async () => {
  await insertLiveClassDB(req.body)
  return res.status(200).send({ Response: "Success." })
});

第二种方式(使用 .catch):

const isSubscribed = new Promise((resolve, reject) => {
  apiGet("/isSubscribed", token).then(async (Response) => {
    if (!isResStatusSubscribed(Response)) reject(res.status(401).send({ errorMessage: "Unauthorized Request." }))
  })
})

const isAdmin = new Promise((resolve, reject) => {
  apiGet("/isAdmin", token).then(async (response) => {
    let isAdmin = response.data.Response.is_admin
    if (!isAdmin) reject(res.status(403).send({ errorMessage: "User is not an Admin" }))
  })
})

Promise.all([isSubscribed, isAdmin])
.then(async () => {
  await insertLiveClassDB(req.body)
  return res.status(200).send({ Response: "Success." })
})
.catch(error => {
  return error
});

我是表达js和写promise.all方法的新手,真的需要帮助。提前谢谢你。

这里有很多地方出了问题。首先,您可以对每个传入的 http 请求发送一个且仅一个响应。因此,您永远不应该并行启动多个异步操作并让每个操作都发送响应。相反,使用 Promise.all() 跟踪两个异步操作并在 Promise.all() 承诺完成时发送响应。

此外,您还有几个 promise 构造函数示例 anti-pattern,您在其中围绕已经 return 是 promise 的函数包装新的 promise。出于多种原因,这被认为是 anti-pattern。不仅是不必要的额外代码(直接return你已有的promise即可),而且在错误处理上也容易出错

以下是我的建议:

// stand-alone functions can be declared in a higher scope and
// used by multiple routes
const isSubscribed = function(token) {
    return apiGet("/isSubscribed", token).then((Response) => {
        if (!isResStatusSubscribed(Response)) {
            // turn into a rejection
            let e = new Error("Unauthorized Request");
            e.status = 401;
            throw e;
        }
    });
}

const isAdmin = function(token) {
    return apiGet("/isAdmin", token).then((response) => {
        let isAdmin = response.data.Response.is_admin
        if (!isAdmin) {
            // turn into a rejection
            let e = new Error("User is not an Admin");
            e.status = 403;
            throw e;
        }
    });
}


// code inside your request handler which you already showed to be async
try {
    await Promise.all([isSubscribed(token), isAdmin(token)]);
    await insertLiveClassDB(req.body);
    return res.status(200).send({ Response: "Success." });
} catch(e) {
    let status = e.status || 500;
    return res.status(status).send({errorMessage: e.message});
}

变更摘要:

  1. isSubscribed()isAdmin() 变成可重复使用的函数,return 是一个承诺。如果他们是订阅者或管理员,该承诺会解决,如果不是,则拒绝,如果 API 有错误,也会拒绝。

  2. 如果这些函数获得成功的 API 响应,但显示它们未订阅或未成为管理员,那么它们将拒绝并使用具有消息集的自定义错误对象,并且具有建议响应状态的自定义 属性 .status 集。

  3. 如果这些函数没有获得成功的 API 响应(API 调用本身失败),那么它将是 apiGet() 拒绝的任何错误对象与.

  4. 然后,await Promise.all([isSubscribed(token), isAdmin(token)]) 如果它解决了,那么它通过了两个测试。如果它拒绝,则它至少未通过一项测试,并且拒绝将以先失败者为准。您可以使用 try/catch 捕获该拒绝以及来自 insertLiveClassDB(req.body) 的任何拒绝。 catch 处理程序然后可以在一处发送错误响应,保证您不会尝试发送多个响应。

  5. 请注意 isSubscribed()isAdmin() 如何测试他们的响应并将 returned 承诺变成 throw e 的拒绝承诺,如果 API响应表示失败。这允许调用代码在一个代码路径中处理所有类型的故障。

我认为没有必要 Promise.all,性能提升将是微乎其微的。如果需要,按顺序执行这两项检查并在每项检查之后抛出错误会更容易,或者如果两个条件都通过则最后响应一次。但这是可行的:

const isSubscribed = async () => {
    const response = await apiGet("/isSubscribed", token);
    if (!isResStatusSubscribed(response)) throw { message : "Unauthorized Request.", status : 401 }; // Will be caught in the Promise.all catch block
}

const isAdmin = async () => {
    const response = await apiGet("/isAdmin", token);
    if (!isResStatusSubscribed(response)) throw { message : "User is not an Admin", status : 403 }; // Will be caught in the Promise.all catch block
}

(async () => {
    try{
        await Promise.all([isSubscribed(), isAdmin()]);
        await insertLiveClassDB(req.body)
        res.status(200).send({ Response: "Success." })
    } catch(err) {
        res.status(err.status).send({ errorMessage : err.message })
    }
})();

Promise.all 只失败一次,只要任何 Promise 失败。因此,即使两个条件都抛出错误,catch 块也只会被触发一次。

如果 isSubscribed 检查失败,则没有必要检查 isAdmin,因此请按顺序进行检查。

通过在检查失败时抛出错误,然后这些或任何其他错误都可以由终端捕获来处理。

我会这样写:

apiGet("/isSubscribed", token)
.then(response => {
    if (!isResStatusSubscribed(response)) {
        throw Object.assign(new Error('Unauthorized Request'), { 'code':401 }); // throw an Error decorated with a 'code' property.
    }
})
.then(() => {
    // arrive here only if the isSubscribed check is successful.
    return apiGet("/isAdmin", token)
    .then(response => {
        if (!response.data.Response.is_admin) {
            throw Object.assign(new Error('User is not an Admin'), { 'code':403 }); // throw an Error decorated with a 'code' property.
        }
    });
})
.then(() => {
    // arrive here only if the isSubscribed and isAdmin checks are successful.
    return insertLiveClassDB(req.body));
}
.then(() => {
    // arrive here only if the isSubscribed and isAdmin checks and insertLiveClassDB() are successful.
    res.status(200).send({ 'Response': 'Success.' });
})
.catch(e) => {
    // arrive here if anything above has thrown.
    // e.code will be 401, 403 or undefined, depending of where the failure occurred.
    res.status(e.code || 500).send({ 'errorMessage': e.message }); // 500 should be a reasonable default.
};