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 后,客户端不再等待该请求。服务端能否及时停止耗时工作,取决于它如何处理连接关闭信号,以及这项工作本身是否支持取消。仅监听客户端事件,不会自动终止数据库查询或其他后台任务。
备注:内容仅供参考。
