← 中文资源

简体中文版本

Node.js — HTTP 响应出现 ERR_STREAM_PREMATURE_CLOSE

排查 Node.js 流水线报 ERR_STREAM_PREMATURE_CLOSE 的原因,区分服务端中断与客户端断开,并修复响应处理与错误分支。

阅读英文原文

症状

ERR_STREAM_PREMATURE_CLOSE 表示流水线中某个流在交付 end 事件之前就 close 了。在代理场景里,同一个错误覆盖两种完全不同的情况:客户端下载到一半断开,或上游在读超时后提前关连接。堆栈通常指向 webstreams 适配器,说明出问题的是 HTTP 响应体这个边界,而不是文件读取。失败瞬间的已发送字节数能区分责任方:字节很少说明是上游问题,大量部分传输则说明是下游客户端离开。

诊断思路

第一步给请求挂 close 日志,记录 bytesWritten:如果 close 在上游响应之前就触发,断开来自客户端,重试逻辑不应打向上游。第二步用 curl –max-time 1 确定性复现客户端中断,预期恰好产生一次 ERR_STREAM_PREMATURE_CLOSE,超过一次说明流水线销毁了多余的流。第三步检查 proxy_read_timeout 等配置:较短读超时叠加慢上游会在没有任何客户端参与的情况下制造同样的错误。

根因

流的可读端以 close 事件而非 end 事件结束,Node 因此无法区分完整响应体与被截断的响应体。当代码没有区分套接字的两个归属方(客户端一侧与上游一侧)时,客户端断开与上游超时都会走同一条错误路径,表现为成片的 premature close。

修复与验证

把代理拆成两半独立处理:客户端一侧监听 aborted/close,用 AbortController 或 body.cancel() 主动取消上游请求,并将其归类为预期行为而非上游 5xx;上游一侧用 stream.pipeline 输送数据,失败时 destroy 响应,让客户端收到明确错误而不是静默的短响应。验证分两个确定性场景:curl 限速 1 秒应只产生一条 cancelled 日志且零上游错误;上游写一半卡住时客户端必须收到错误。两种情况下都核对 res.bytesWritten 与上游 Content-Length 是否精确对账。

代码示例

Error [ERR_STREAM_PREMATURE_CLOSE]: Premature close
    at node:internal/webstreams/adapters.js:481:18
app.get('/large-file', async (req, res) => {
  const upstream = await fetch(upstreamUrl);
  req.on('close', () => {
    if (!res.writableEnded) {
      log.info({ id: req.id, bytes: res.bytesWritten }, 'client aborted');
      upstream.body.cancel().catch(() => {});
    }
  });
  await pipeline(Readable.fromWeb(upstream.body), res)
    .catch((err) => {
      if (!res.writableEnded) res.destroy(err);
    });
});