先记住这个答案
req(IncomingMessage)是Readable流,res(ServerResponse)是Writable流。当底层连接异常中断或写入失败,流会emit('error')。若没有监听器,该事件会作为未处理的错误被抛出到事件循环,进程默认打印堆栈后退出。req的错误多来自客户端断开或解析异常,res的错误多来自对端关闭后继续写入。只监听其中一个,另一个出错时依旧崩溃。故应同时为req和res绑定error处理程序,并在回调中清理资源与互相终止,确保单个请求错误不拖垮整个进程。
- req和res是流,未监听error会抛出导致崩溃
- req错误来自接收中断,res错误来自写入失败
- 必须分别监听并处理,才能保证服务稳定
为什么'error'事件必须处理?
Node.js的HTTP请求对象req(IncomingMessage)和响应对象res(ServerResponse)分别是可读流和可写流。流在内部发生异常时会发出'error'事件。如果该事件没有挂任何监听器,Node就像对待普通EventEmitter一样,会将错误作为未捕获异常在事件循环中抛出。这个异常不会被后续的Promise.catch或try/catch逮住,进程只能打印堆栈并退出。
req和res的错误源截然不同:req可能因客户端提前断开、报文解析失败或内容编码问题报错;res则可能在底层socket已关闭后仍尝试写入数据、或由于write超时被置错。业务代码若只监听请求的'aborted'事件或用try包裹async路由,往往无法覆盖这些流级错误,所以必须直接为每个流挂error处理器。
上传中断场景中的两个崩溃点
假设服务接收POST大文件,用req.on('data')收集字节,客户端传到一半突然断网,TCP层会通知本端连接异常。该req流随即触发'error';如果代码没有监听req的error,进程会立刻退出,殃及正在处理的其它请求。更隐蔽的是,因为底层socket已失效,本服务向res写入返回体也会触发另一个'error'——即使req的error被提前处理,res上漏监听依然会让进程崩溃。
可靠处理是:在请求刚开始就同时挂req.on('error', handleError)和res.on('error', handleError),handleError里记录错因并做清理:如clearTimeout、删除临时文件,然后调用req.destroy()或res.destroy()关闭对方。还要避免在错误回调里再次触发可能失败的IO操作。经这样防护后,单个客户端异常只会作废当前请求,不能影响同进程的其他请求。
哪些情况仍会漏掉或冲突?
第一种常见失效是:把req或res交给框架或封装函数(如stream.pipeline、finished)时,工具库已接管了错误,此时若再在外部额外监听,虽然不至于冲突,但不保证能回调到自己的监听——因为流的error可能已被内部消费。真正会漏的是自己用.on('data')、.write或.pipe的裸流代码,只要没手动绑error,错误就会向上穿透。
第二种误区是:在某个时机过早移除监听,或只在第一次error后清理就认为安全。要记住error事件可能在不同时点出现,只要请求未结束,监听应保持有效。实现上可以在开始传输前绑定,在req/res任一触发'close'后再设标志位,后续事件直接忽略。代价是每个请求多两个闭包与一个清理函数,换取的是明确失败边界和进程存活保障。
容易答错的地方
- 只监听res的error即可
- 会漏掉req侧错误。req是独立可读流,其解析、接收失败会emit('error'),不监听同样导致未处理异常。例如客户端在body未读完就RST时,req会先error,若漏监听则进程崩溃。
- 在error回调里再调用res.end兜底
- res可能已经销毁,end()会再触发一次error或抛异常。应该在回调里先判断writable,再用destroy或直接销毁socket,同时避免重复调用。记录日志后及时清理资源才是重点。
面试官还会怎么问?
使用stream.pipeline()后还需要手动监听req和res的error吗?
pipeline()会把所有流的错误汇总到其回调中,并自动销毁相关流。若提供了非空回调,错误不会成为未处理异常,但管道完成前还可能自行触发error,建议仍挂载并只做日志和资源清理。
'error'事件与'aborted'事件在HTTP请求上的区别是什么?
'aborted'只表示客户端中止请求,不一定会触发'error';但一些底层TCP错误会同时导致'aborted'与'error'。Node.js流规范要求只有无监听时error才会抛异常,'aborted'没有这个特性。判断中止仍需两者结合。
错误发生时应该调用res.end()还是socket.destroy()?
推荐先检查socket状态,用req.destroy()或res.destroy()直接切断底层连接,因为end()需要排队写结束帧,在已损坏连接上会立即失败或卡死。同时注意不要重复销毁,销毁后所有后续写操作应被忽略。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。