Node.js HTTP服务器优雅关闭异常:server.close回调未触发
问题场景
基于Node.js v20.4.0内置的http.Server实现服务器,需求是收到SIGTERM或SIGINT信号时,停止接收新连接、完成正在处理的请求后退出。简化代码(server.js)如下:
const http = require("http"); const requestListener = (_, res) => { res.writeHead(200); res.end("howdy!"); }; const server = http.createServer(requestListener); const host = 'localhost' const port = 8888 const shutdown = () => { console.log(`Got SIGINT, calling server.close() ...`); server.close(() => { console.log('Server closed, exiting') process.exit(0) }) } process.on("SIGINT", shutdown) server.listen(port, host, () => { console.log(`Listening on http://${host}:${port}`); });
异常现象
直接按Ctrl+C发送SIGINT能正常触发退出流程,但按以下步骤操作时,server.close()的回调始终不执行:
- 执行
node server.js启动服务器 - 在Chrome标签页打开
http://localhost:8888 - 在服务器终端按Ctrl+C
此时终端会打印Got SIGINT, calling server.close() ...,但直到关闭Chrome标签页,才会触发Server closed, exiting并退出。
原因分析
Chrome发送请求时携带了Connection: keep-alive请求头,Node服务器响应也返回了Connection: keep-alive字段,建立了HTTP长连接。根据server.close()的官方文档,该方法仅会停止接收新连接,并关闭未处于请求发送/响应等待状态的连接,但Keep-Alive长连接在请求处理完成后会进入空闲等待状态,不会被自动关闭,导致服务器一直持有这些连接,server.close()的回调无法触发。这对应Node.js仓库的#41578问题,且server.closeIdleConnections()方法也无法解决该问题。
解决方案
方案1:手动关闭空闲长连接(推荐)
修改shutdown函数,在调用server.close()后,主动遍历服务器的所有连接,关闭那些已经完成请求处理的空闲连接:
const shutdown = () => { console.log(`Got SIGINT, calling server.close() ...`); // 停止接收新连接 server.close(() => { console.log('Server closed, exiting') process.exit(0) }); // 遍历并关闭空闲的Keep-Alive连接 server.getConnections((err, connectionsCount) => { if (err) throw err; console.log(`Closing ${connectionsCount} idle connections`); // 访问服务器的内部连接列表 server._connections.forEach((conn) => { // _httpMessage为null表示当前连接没有正在处理的请求,处于空闲状态 if (!conn._httpMessage) { conn.end(); } }); }); };
该方案保留了HTTP长连接的性能优势,同时能在收到退出信号时正常关闭服务器。
方案2:禁用Keep-Alive(简单但牺牲性能)
在响应时强制设置Connection: close头,让每次请求完成后直接关闭连接:
const requestListener = (_, res) => { res.writeHead(200, { 'Connection': 'close' }); res.end("howdy!"); };
此方法实现简单,但会禁用HTTP长连接,影响高频请求场景下的性能。
内容的提问来源于stack exchange,提问作者grumbler

