Node.js Express 应用程序处理启动错误

34

我有一个使用Node.js和Express的应用程序,我需要为它编写测试。我在处理Express应用程序错误时遇到了问题。我找到了这个如何捕获类似EADDRINUSE的node.js/express服务器错误?,但对我没有用,我不知道为什么。我想要处理expressApp.listen()执行时可能出现的错误(例如EADDRINUSE、EACCES等)。

express = require('express')
listener = express()

#doesn't work for me
listener.on('uncaughtException', (err) ->
  #do something
)

#doesn't work too
listener.on("error", (err) ->
  #do something
)

#this works, but it caughts all errors in process, I want only in listener
process.on('uncaughtException', (err) ->
  #do something
)

listener.listen(80) #for example 80 to get error

有什么想法吗?

listener.on 'error', ... 应该可以工作。即使有这行代码,它是否只会执行正常的堆栈跟踪并崩溃呢? - loganfsmyth
是的,如果我执行这个 'listener.listen(80)',它会打印堆栈跟踪并崩溃。即使使用 'listener.on' 'error',...' 也许在这种情况下发生的错误不是Express错误,这就是为什么它无法处理的原因。但这只是假设。 - Piane_Ramso
4个回答

112

这应该就可以解决问题:

listener.listen(80).on('error', function(err) { });

listener.listen的实际作用是创建一个HTTP服务器并在其上调用listen:

app.listen = function(){
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

21
这个答案是最简单和正确的,而且没有赞!真是不可思议!关键是要理解app.listen返回的是webServer对象。我用这个代码: console.log('success'); }).on('error', function(err) { if (err.errno === 'EADDRINUSE') { console.log('port busy'); } else { console.log(err); } }); - david_p
我不理解你的代码在做什么。如果你正在调用app.listen,那么Node在幕后已经执行了http.createServer,我认为你是在覆盖原生的app.listen()方法并用自己的代码进行重写吗? - PositiveGuy
3
应该使用的是第一行代码,其他所有内容都是解释为什么它能实现。 - Marius Tibeica
谢谢Mariuis Tibeica!我将您的代码模式应用到我的express应用程序中,现在可以捕获EADDRINUSE错误了。之前我的应用程序会在没有捕获try块中的catch错误的情况下崩溃。直接将listen(port).on('error',...)处理程序添加到listen中就解决了问题。我在下面单独回答中记录了我的代码,以便格式更好。 - Dennis G Allard

29

首先,expressJS不会触发uncaughtException事件,而是process会触发该事件,所以你的代码无法正常工作并不令人意外。

因此,请改用:process.on('uncaughtException',handler)

其次,expressJS已经提供了一种标准的错误处理方式,即使用其为此目的提供的中间件函数,例如:

app.configure(function(){
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

该函数会向客户端返回一个带有可选堆栈跟踪信息的错误消息,并在connectJS errorHandler文档中进行了说明。
(注意,errorHandler实际上是connectJS的一部分,只有通过expressJS才能访问。)
如果现有的errorHandler提供的行为不符合您的需求,它的源代码位于connectJS的errorHandler中间件,可以轻松修改以适应您的需要。
当然,与其直接修改此函数,更好的方法是使用connectJS版本作为起点创建自己的errorHandler,如下所示:
var myErrorHandler = function(err, req, res, next){
    ...
    // note, using the typical middleware pattern, we'd call next() here, but 
    // since this handler is a "provider", i.e. it terminates the request, we 
    // do not.
};

将其安装到expressJS中:

app.configure(function(){
    app.use(myErrorHandler);
});

请见《连接吧,快点》了解connectJS的filterprovider中间件的概念,参考如何为Connect/Express编写中间件获取一个良好编写的教程。
您还可能会发现以下内容有用: 最后,在其自己的测试中可以找到关于测试expressJS的优秀信息资源。

感谢您提供如此详细的解释,这对我很有用。在处理网络请求时,我不需要处理错误。我需要在创建服务器并调用app.listen(...)时进行处理。现在我明白了,在这里需要处理Node.js错误,而不是Express中间件。在我的情况下,process.on('uncaughtException',handler) 是可接受的。 - Piane_Ramso
@Piane_Ramso,如果您感兴趣的话,向process.on('uncaughtException')添加自己的事件处理程序绝对是正确的方法。如果您好奇的话,处理“未捕获”异常的底层代码位于(或接近)node.cc的第1739行。 - Rob Raisch
天啊,这太复杂了。 - PositiveGuy

15

提到: Marius Tibeica的答案很完整和出色,david_p的评论也是。Rob Raisch的答案也不错(有趣的探索)。https://dev59.com/GWYr5IYBdhLWcg3w499-#27040451
https://dev59.com/GWYr5IYBdhLWcg3w499-#13326769

注意

这个第一种方法是糟糕的!我将其保留作为参考!请看更新部分!了解更好的版本!也了解更好的解释!

糟糕版本

对于那些会发现这个函数有用的人们,在这里有一个实现忙端口处理的函数(如果端口忙碌,则会尝试下一个端口,直到找到没有忙碌的端口)。

app.portNumber = 4000;
function listen(port) {
    app.portNumber = port;
    app.listen(port, () => {
        console.log("server is running on port :" + app.portNumber);
    }).on('error', function (err) {
        if(err.errno === 'EADDRINUSE') {
            console.log(`----- Port ${port} is busy, trying with port ${port + 1} -----`);
            listen(port + 1)
        } else {
            console.log(err);
        }
    });
}

listen(app.portNumber);

当端口繁忙时,函数 listen 会对自身进行递归调用,并每次将端口号加一。

完全重新做过的更新

回调函数完整版本

首先,这个版本遵循与 Node.js 中 http.Server.listen() 方法相同的签名!

function listen(server) {
    const args = Array.from(arguments);
    // __________________________________ overriding the callback method (closure to pass port)
    const lastArgIndex = arguments.length - 1;
    let port = args[1];
    if (typeof args[lastArgIndex] === 'function') {
        const callback = args[lastArgIndex];

        args[lastArgIndex] = function () {
            callback(port);
        }
    }

    const serverInstance = server.listen.apply(server, args.slice(1))
        .on('error', function (err) {
            if(err.errno === 'EADDRINUSE') {
                console.log(`----- Port ${port} is busy, trying with port ${port + 1} -----`);
                port += 1;
                serverInstance.listen.apply(serverInstance, [port].concat(args.slice(2, lastArgIndex)));
            } else {
                console.log(err);
            }
        });

    return serverInstance;
}

签名:

listen(serverOrExpressApp, [port[, host[, backlog]]][, callback])

https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback相同。

回调函数的签名已更改为

(port) => void

用法:

const server = listen(app, 3000, (port) => {
    console.log("server is running on port :" + port);
});

// _____________ another example port and host
const server = listen(app, 3000, 'localhost', (port) => {
    console.log("server is running on port :" + port);
});

说明

与旧示例相反!此方法不会调用自身!

关键要素:

  • app.listen() 的第一个调用将返回一个 net.Server 实例
  • 在绑定事件后,再次调用 listen 到同一 net.Server 实例将尝试重新连接!
  • 错误事件侦听器始终存在!
  • 每次发生错误时,我们都会重新尝试。
  • 端口变量在回调的闭包中起作用!当回调被调用时,正确的值将被传递。

重要的是

serverInstance.listen.apply(serverInstance, [port].concat(args.slice(2, lastArgIndex)));

为什么我们在这里跳过了回调函数!?

一旦添加了回调函数!它会在服务器实例内部保留在一个数组中!如果我们添加另一个!就会有多个触发器!次数为(尝试次数+1)。所以我们只在第一次尝试中包含它!

这样我们就可以直接返回服务器实例!并继续使用它来尝试!并且这样做很干净!

仅简单版本端口

这也可以帮助更好地一览

function listen(server, port, callback) {
    const serverInstance = server.listen(port, () => { callback(port) })
        .on('error', function (err) {
            if(err.errno === 'EADDRINUSE') {
                console.log(`----- Port ${port} is busy, trying with port ${port + 1} -----`);
                port += 1;
                serverInstance.listen(port);
            } else {
                console.log(err);
            }
        });

    return serverInstance;
}

这里的参数port变量在闭包中发挥作用!

ES6全版本

function listen(server, ...args) {
    // __________________________________ overriding the callback method (closure to pass port)
    const lastArgIndex = args.length - 1;
    let port = args[0];
    if (typeof args[lastArgIndex] === 'function') {
        const callback = args[lastArgIndex];

        args[lastArgIndex] = function () {
            callback(port);
        }
    }

    const serverInstance = server.listen(server, ...args)
        .on('error', function (err) {
            if(err.errno === 'EADDRINUSE') {
                console.log(`----- Port ${port} is busy, trying with port ${port + 1} -----`);
                port += 1;
                serverInstance.listen(...[port, ...args.slice(1, lastArgIndex)])
            } else {
                console.log(err);
            }
        });

    return serverInstance;
}

为什么旧版本不好

实际上并不是那样!但是在第一个版本中!每次失败时我们都调用函数本身!而且每次它都创建一个新的实例!垃圾回收器将要忙碌一些!

这并不重要,因为这个函数只执行一次,在开始时执行!

旧版本没有返回服务器实例!

附加信息(针对@sakib11)

您可以查看@sakib11的评论,了解他遇到的问题!这可能很有启发性!

还在评论中,我提到了promise版本和闭包getter模式!我认为它们并不有趣!以上方法只需遵守与nodejs相同的签名即可!而太多的回调也没问题!我们立即获得了服务器引用!通过promise版本!会返回一个promise,解决后我们传递所有元素!serverInstance + 端口!

如果您想了解闭包getter模式!(在这里不好)

在我们的方法内部,我们创建一个引用,引用服务器实例!如果我们无法像现在这样返回服务器实例(假设不可能!所以每次都会创建一个新实例!),则该模式包括创建一个闭包(该范围内的方法)并返回它!

因此,对于用法:

const getServer = listen(port, () => {
   console.log('Server running at port ' + getServer().address().port);
   const io = socketIo(getServer(), {});
}); 

但这只是额外的开销,特别是我们需要等待服务器完成!除非我们以使用回调函数或返回 Promise 的方式设置它!

这只会使问题变得更加复杂!根本不好!

只是因为我提到了它!

而且上述方法可以进行调整!添加尝试次数限制!并添加一些事件或钩子!但是嗯!通常我们只需要一个简单的函数来尝试并使其成功!对我来说,以上内容已经足够了!

好链接

来自文档

app.listen() 方法返回一个 http.Server 对象,(对于 HTTP)它是以下方便方法:

app.listen = function () {
  var server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

那个完美的解决方案解决了我的问题,谢谢兄弟。 - Hiep Tran
很高兴听到这个。我很荣幸。谢谢。 - Mohamed Allal
我之前尝试了类似的东西,递归部分的工作方式确实如此,但我还试图返回app.listen以将其传递给socket.io,但由于某种原因它不起作用。 我猜是附加错误事件处理程序返回了不同的内容? - sakib11
你是如何返回值的!在我的例子中,如果我们返回第一个app.listen().on(),我们将得到一个服务器实例!但如果失败了!重试的回调函数将被调用!并且将创建一个新的服务器实例!所以这应该是你的问题!app.listen和所有的.on链接都将返回一个net.Server实例!你有主要两个选项!使用http.createServer。获取你的服务器实例!然后在我的函数中,用server.listen代替app.listen!并将其附加到socket.io!你有参考! - Mohamed Allal
否则,如果我们想在函数内部完成它!那么我们有很多选择!使用onSuccess回调或者Promise可能是最简单的!您可以在resolve中传递成功附加的服务器! - Mohamed Allal
显示剩余3条评论

0

谢谢Marius Tibeica!

var app = express();
const port = process.env.PORT || 3000

我的原始代码无法捕获EADDRINUSE错误:

try {
    app.listen(port)
    console.log('Listening on port ' + port)
} catch (err) {
    console.log('ERROR: ' + err)
    console.log('Exiting...')
}

根据Marius的建议,我的代码得到了改进,现在可以捕获EADDRINUSE错误:

try {
    app.listen(port).on('error', (err) => {
        console.log('ERROR: ' + err)
        console.log('Exiting...')
    })
    console.log('Listening on port ' + port)
} catch (err) {
    console.log('ERROR: ' + err)
    console.log('Exiting...')
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接