先记住这个答案
中间件靠 next() 移交控制,或调用 res.end/res.send 结束请求。若两者皆无,请求永不结束:后续中间件和路由不会执行,响应不发,连接保持打开且不会自动释放,客户端超时后服务端资源也未必及时清理。定位时加入口日志和出口日志,或包一个 setTimeout 中间件,超时后打印当前栈和 URL 即可找出漏掉 next() 的处理函数。
- 中间件必须以 res 或 next 结束控制流
- 挂起连接不会自动释放,超时后仍可能占资源
- 日志与超时中间件能快速定位漏调函数
为什么请求会挂起
Express 的中间件栈要求每层完成后调用 next() 移交,或通过 res 发送响应结束。若两者都缺,控制流停在该层,后续中间件与路由不会运行,也没有响应返回。连接保持打开,等待永远不会发生的数据。
从事件循环看,该请求的 socket 没有被关闭,服务器资源被长期占用。虽然不会抛出异常,但高并发下挂起请求会耗尽文件描述符,导致新连接被拒。给每个中间件入口和出口打日志,能直观看出停在哪个函数。
用户校验服务中的漏调分支
用户模块的校验中间件对请求头 token 异步解码,成功后查询数据库用户,若查到则 req.user 赋值并调用 next();若查不到,代码只写了一条日志后 return,没有 next 也没发 res。当客户端携带不存在的 token 时,请求挂起无响应。
排查时先在该中间件前后打印标记,前端发现请求在第二个标记前消失,说明卡在回调内。接着给 app.use 挂一个超时包装器,用 setTimeout 在 8 秒后 res.status(504).send('timeout') 并输出调用栈,日志显示当前中间件名,修复方法是查询为空时补 res.status(404).json(...) 或调用 next(),但为符合语义,补响应是正解。
适用边界与失效情形
若中间件已经调用 res.end(),即使没调 next 也不会挂起,因为响应结束已使 HTTP 周期完成。若中间件内部发生异步异常但未传给 next,也可能出现同类无响应现象,但那是错误处理缺陷而非简单的 next 漏调。超时中间件只能捕捉同步路径上的截断,对于嵌套太深的 Promise 链路需要额外包裹。
客户端超时断开后,服务端未必立刻关闭 socket,挂起连接可能仍占着资源。部署在反向代理后可用代理的 upstream_timeout 兜底,但应用层仍应在 res 的 close 事件里做清理。要彻底避免,可在开发环境强制所有中间件代码 lint,禁止既不 send 也不调用 next 的函数存在。
容易答错的地方
- 以为 next 必须调用才正确
- 事实上若已经发送响应,不调用 next() 也允许,只是会让当前中间件成为终点。常见误区是认为不调用 next 必错,却忽略了需要这种短路场景。真正的错误是既未发响应又未调用 next,造成后续无从清理。
- 客户端超时即可清理连接
- 客户端超时只是停止等待,TCP 连接是否关闭取决于底层 keepAlive 和 Node 设置,如果没显式结束,服务端 socket 不会被释放。大量挂起连接会耗尽文件描述符,最终必须重启进程。生产环境要主动监测半关闭连接。
面试官还会怎么问?
如何全局捕获请求挂起而不修改每个中间件?
可把整个业务中间件包在 async 函数里,用 Promise.race 与超时相争,超时后打印堆栈并强制响应。但 Express 4 的 next 不是 Promise 兼容,需配合包装函数。
调用 next('route') 与不调用 next 有何不同?
next('route') 明确跳转到下一个同方法路由,控制权仍在传递,不会挂起;不调用 next 则是控制流完全终止。注意 next('route') 仅适用于通过 app.METHOD 或 router.METHOD 加载的中间件,不适用于 app.use。
用浏览器访问挂起请求,多久会得到错误?
取决于客户端超时设置。浏览器通常约 30 秒到 1 分钟,curl 无限等待。服务端若长期无响应,nginx 默认 proxy_read_timeout 为 60 秒,但不等同于 Node 释放连接。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。