PAYATHON 2026

Fetch POST 被取消时,如何判断 Express 客户端已断开?

数据小陈

结论

可以用 AbortController 取消 Fetch POST 请求。常见问题是服务端用 req.on('close') 判断客户端是否中止。Express 路由运行前,bodyParser.json() 已经读取并解析了请求体。此时 req 的 close 事件可能表示请求消息已处理完毕,不能单独用来判断客户端是否取消请求。

要判断客户端是否在等待响应时断开,可以监听响应对象 res 的 close 事件,并检查 res.writableEnded。只有响应尚未正常结束时,才将其视为连接提前关闭。

为什么 POST 请求会触发 req 的 close

问题中的中间件顺序是先运行 jsonParser,再进入路由回调并注册 req.on('close', handleClose)。这时 JSON 请求体通常已经接收并解析完毕。Node.js 的 IncomingMessage 上的 close 事件也可能表示请求消息处理完成,因此不能只靠它判断客户端是否取消了请求。

POST 请求带有请求体,但这不代表请求对象会一直保持打开,直到服务端返回响应。即使客户端还在等响应,请求体也可能早已发送完毕。

在 Express 中检测提前断开

可以监听响应对象的 close 事件,并在响应未正常结束时取消正在进行的工作:

router.post('/test', jsonParser, async (req, res, next) => {
  const controller = new AbortController();

  res.on('close', () => {
    if (!res.writableEnded) {
      controller.abort();
      console.log('client disconnected');
    }
  });

  try {
    await delay(10000, controller.signal);
    if (!res.destroyed) {
      res.send();
    }
  } catch (error) {
    if (controller.signal.aborted) {
      return;
    }
    next(error);
  }
});

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      reject(new Error('Operation aborted'));
      return;
    }

    const timer = setTimeout(() => {
      signal.removeEventListener('abort', onAbort);
      resolve();
    }, ms);

    function onAbort() {
      clearTimeout(timer);
      reject(new Error('Operation aborted'));
    }

    signal.addEventListener('abort', onAbort, { once: true });
  });
}

res.on('close') 可能在响应正常完成或连接提前关闭时触发。代码通过 !res.writableEnded 排除已经正常结束的响应。收到连接提前关闭的信号后,controller.abort() 会取消这里的 delay。在实际业务中,也可以把 controller.signal 传给支持 AbortSignal 的下游操作,并在取消时清理相关资源。

客户端处理取消结果

调用 controller.abort() 后,Fetch 通常会以 AbortError 拒绝。捕获这个结果,可以避免把用户主动取消当成未处理异常:

try {
  const response = await fetch('/test', {
    body: JSON.stringify({ one: 1 }),
    headers: { 'Content-Type': 'application/json' },
    method: 'POST',
    signal: controller.signal
  });

  console.log('the end');
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('request cancelled');
  } else {
    throw error;
  }
}

取消 Fetch 后,客户端不再等待该请求。服务端能否及时停止耗时工作,取决于它如何处理连接关闭信号,以及这项工作本身是否支持取消。仅监听客户端事件,不会自动终止数据库查询或其他后台任务。

备注:内容仅供参考。