【问题标题】:Express Error handling middleware for production and development用于生产和开发的 Express 错误处理中间件
【发布时间】:2021-05-29 02:39:03
【问题描述】:

我正在尝试将生产错误日志设为默认值,并仅在 environment variabledevelopment 时向用户显示其他内容。我正在尝试通过以下方式执行此操作,但我收到一条消息说Cannot set headers after they are sent to the client

    const ErrorClass = require('../routes/utils/ErrorClass');

const prodDBCastError = err => {
    const message = `Invalid ${err.path}: ${err.value}`;
    return new ErrorClass(message, 400);
};

const prodDBDuplicateFieldsError = err => {
    const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];

    const message = `Duplicate field value: ${value}.`;
    return new ErrorClass(message, 400);
};

const prodDBDValidationError = err => {
    const errors = Object.values(err.errors).map(el => el.message);

    const message = `Invalid input data. ${errors.join('. ')}`;
    return new ErrorClass(message, 400);
};

const handleBadRequestDB = err => {
    const errors = err.message;
    const message = `Fixes: ${errors}`;
    return new ErrorClass(message, 400);
};

const sendProdError = (err, res) => {
    if (err.isOperationalError){
        res.status(err.status).json({
            status: err.status,
            message: err.message,
        });
    } else {
        res.status(500).json({
            status: 'error',
            message: 'Server Issue.',
        });
    }
};

const sendVerboseDevError = (err, res) => {

    logger.error(err);
    err.status = err.status || 500;
    res.status(err.status).json({
        status: err.status,
        name: err.name,
        path: err.path,
        errors: err.errors,
        message: err.message,
        stack: err.stack,
    });
};

module.exports = (err, req, res, next) => {

    if (process.env.APP_ENV === 'development'){
        sendVerboseDevError(err, res);
    }
    if (err.name === 'CastError') {err = prodDBCastError(err);}
    if (err.name === 'MongoError') {err = prodDBDuplicateFieldsError(err);}
    if (err.name === 'ValidationError') {err = prodDBDValidationError(err);}
    if (err.name === 'Bad Request') {err = handleBadRequestDB(err);}

    sendProdError(err, res);
};

这就是我的 ErrorClass 的样子:

class ErrorClass extends Error {
    constructor(message, status) {
        super(message);

        this.status = status;
        this.isOperationalError = true;

        Error.captureStackTrace(this, this.constructor);
    }
}
module.exports = ErrorClass;

【问题讨论】:

  • 在您的错误处理程序中,您同时调用了sendVerboseDevError(err, res);sendProdError(err, res);,这意味着您调用了两次res.status(..).json(..),但在第一次调用后,与客户端的连接已关闭,无法发送更多数据这会导致错误。所以只调用其中一个函数,永远不要同时调用。
  • 我想不出任何其他方法可以将sendProdError 作为基本情况并始终发送输出而不泄露太多细节,除非env vardevelopment

标签: javascript node.js mongodb express error-handling


【解决方案1】:

我建议您像这样更改您的代码。

首先,您避免多次调用res.json,并且您只检查应用程序是否在开发模式下运行一次。无需对每个请求进行检查。

var devHandler = (err, req, res, next) => {
    logger.error(err);
    err.status = err.status || 500;
    res.status(err.status).json({
        status: err.status,
        name: err.name,
        path: err.path,
        errors: err.errors,
        message: err.message,
        stack: err.stack,
    });
};

var prodHandler = (err, req, res, next) => {
    
    if (err.name === 'CastError') {err = prodDBCastError(err);}
    if (err.name === 'MongoError') {err = prodDBDuplicateFieldsError(err);}
    if (err.name === 'ValidationError') {err = prodDBDValidationError(err);}
    if (err.name === 'Bad Request') {err = handleBadRequestDB(err);}

    if (err.isOperationalError){
        res.status(err.status).json({
            status: err.status,
            message: err.message,
        });
    } else {
        res.status(500).json({
            status: 'error',
            message: 'Server Issue.',
        });
    }
};

module.exports = process.env.APP_ENV === 'development' ? devHandler : prodHandler;

【讨论】:

  • 这太棒了,效果很好,只调用一次开发就很有意义,但是如何在 prodHandler 一个函数中检查 env var 并使其成为单一函数入口点,然后相应地发送输出?我在处理程序中包含了一个`if`块,用于检查env var,如果为真则调用devHandler,但这再次发送相同的错误
  • APP_ENV 值在应用运行时不会改变,为什么要包含 if 语句?
  • module.exports = prodHandler 这样做使其成为错误的单一入口
  • 在您的代码中,您只需在sendVerboseDevError(err, res); 之后或在其前面添加return,就像if (process.env.APP_ENV === 'development') return sendVerboseDevError(err, res); 一样,以防止其余代码运行。
  • 这是我一直以来犯的愚蠢错误,非常感谢!
【解决方案2】:

这是因为您在从 sendVerboseDevError 发送数据后尝试从 sendProdError 发送数据。 res.json 正在向客户端发送 json。

这里解释了这背后的原因https://stackoverflow.com/a/7086621/2232902

Express 中的 res 对象是 Node.js 的子类 http.ServerResponse(阅读 http.js 源代码)。你可以打电话 res.setHeader(name, value) 任意次数,直到您调用 res.writeHead(状态码)。在 writeHead 之后,headers 被烘焙进去 而且只能调用res.write(data),最后调用res.end(data)

我建议您将sendProdErrorsendVerboseDevError 修改为constructProdErrorconstructVerboseDevError,然后从代码中的同一点发送。

参考:https://stackoverflow.com/a/733858/2232902

【讨论】:

    【解决方案3】:

    我认为这里的问题在于多个 if 条件。 如果第一个条件为真,则将执行“sendVerboseDevError”函数。 其中有以下代码

    res.status(err.status).json({
        status: err.status,
        name: err.name,
        path: err.path,
        errors: err.errors,
        message: err.message,
        stack: err.stack,
    });
    

    在这个函数之后,响应头将被设置。 然后,如果条件为真,则流程进入其余的 if 条件,然后再次调用某个其他函数,该函数试图再次设置响应。 这就是为什么您收到错误“在将标头发送到客户端后无法设置标头”的原因 设置响应后,您需要调用“next()”方法。 所以你应该在每个 if 条件的底部添加“next()”方法。

    类似

        module.exports = (err, req, res, next) => {
    
        if (process.env.APP_ENV === 'development'){
            sendVerboseDevError(err, res);
            next();
        }
        if (err.name === 'CastError') {
            err = prodDBCastError(err);
            next();
        }
        if (err.name === 'MongoError') {
            err = prodDBDuplicateFieldsError(err);
            next();
        }
        if (err.name === 'ValidationError') {
            err = prodDBDValidationError(err);
            next();
        }
        if (err.name === 'Bad Request') {
            err = handleBadRequestDB(err);
            next();
        }
    
        sendProdError(err, res);
        next();
    };
    

    下一个函数将终止 API 调用并返回响应,并防止它在响应由当前执行的任何函数设置后再次设置响应。

    P.S: 你也可以调用“res.end()”而不是“next()”函数。

    【讨论】:

    • next() 函数究竟是如何阻止调用 sendVerboseDevError(err, res);sendProdError(err, res); 的?在您的代码中,sendVerboseDevError(err, res);sendProdError(err, res); 都将在开发模式下被调用,就像在问题中一样。
    • 我在该页面上尝试了示例 2 中的代码,它会导致相同的错误 Cannot set headers after ... 您根本无法调用 res.send 和/或 res.json 两次,next 不会阻止这种情况任何方式。
    猜你喜欢
    • 1970-01-01
    • 2012-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-28
    • 2018-06-19
    • 1970-01-01
    • 2018-04-01
    相关资源
    最近更新 更多