关于本文
本文是通过利用生成式 AI 的自动化生成流程创建的。我们查阅了 MDN Web Docs 以及 Node.js 的 AbortController 和 fetch 规范,并准备了一个无需外部 API 即可对比中断与正常结束的 localhost 实验。目前尚未在 Node.js 实机上进行运行验证。
验证状态:📘 已确认 MDN 及 Node.js 官方规范 / 尚未进行实机验证
在使用 JavaScript 的fetch()调用 API 时,有时会遇到对方服务器无响应、画面停滞不前的情况。这时就可以派上用场的是AbortController。它可以在经过一定时间后中断请求,或者在用户点击取消按钮时停止处理。
然而,中断通信与取消服务器端的任务是两码事。例如,在向订单 API 发送请求后关闭页面,服务器端的订单处理仍有可能已经成功。在实际业务处理中,需要另外进行防止重复提交和结果确认。
AbortController 的机制
AbortController中包含一个signal这样的对象。将该 signal 传递给 fetch,稍后调用abort()时,处于等待状态的 fetch 就会被中断。
const controller = new AbortController();
const responsePromise = fetch("/api/data", {
signal: controller.signal
});
// 必要になったときに中断する
controller.abort();
这只是一个展示形态的示例,/api/data并不是直接复制到不存在该对象的页面中就能运行的代码。为了进行实际对比,接下来我们将在 Node.js 上搭建一个小型本地服务器。
可直接尝试的 localhost 演示
请使用 Node.js 18 或更高版本。将文件名命名为abort-demo.js保存为。
const http = require("node:http");
async function main() {
const server = http.createServer((_req, res) => {
// サーバー側は400ミリ秒かけて応答する
setTimeout(() => {
if (!res.writableEnded) {
res.writeHead(200, {"content-type": "text/plain"});
res.end("finished");
}
}, 400);
});
// 外部へ公開しないよう localhost のみに待ち受ける
server.listen(0, "127.0.0.1");
await new Promise(resolve => server.once("listening", resolve));
const url = `http://127.0.0.1:${server.address().port}/`;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 100);
try {
await fetch(url, {signal: controller.signal});
console.log("Unexpectedly completed before abort");
process.exitCode = 1;
} catch (error) {
if (error.name !== "AbortError") throw error;
console.log("First request: AbortError (cancelled by client)");
} finally {
clearTimeout(timer);
}
// 同じサーバーへ今度は中断せずにアクセスする
const response = await fetch(url);
console.log(`Second request: ${response.status} ${await response.text()}`);
} finally {
server.closeAllConnections?.();
await new Promise(resolve => server.close(resolve));
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
server.listen(0)用于让操作系统选择一个空闲端口。由于不固定端口号,因此可以减少与其他本地服务的冲突。
node abort-demo.js
预期结果如下。这是根据 API 规范预期的内容,而不是实际设备日志。
First request: AbortError (cancelled by client) Second request: 200 finished
该实验使服务器等待 400 毫秒,在仅 100 毫秒时中断第一次客户端请求,第二次请求则不中断一直获取到结束。由于可以比较对同一台本地服务器的两次访问,因此不需要将实际的网络通信发送到外部。
GitHub 示例:AbortController 与 localhost fetch中保存了代码和运行方法。
为什么要在 finally 中停止定时器?
setTimeout如果残留,可能会对已经完成的 fetch 稍后执行abort()。无论成功还是失败,不再需要的定时器都应该在clearTimeout中进行清理。
此外,该示例还会停止服务器。这是为了避免在复制文章中的代码并多次尝试时,服务器在后台持续运行。
修改一处以查看差异
100将600修改为然后进行尝试。
// 変更前 const timer = setTimeout(() => controller.abort(), 100); // 変更後 const timer = setTimeout(() => controller.abort(), 600);
由于服务器的响应大约为 400 毫秒,如果将其设为 600 毫秒,fetch 可能会在中断之前完成,并可能显示示例中的“Unexpectedly completed before abort”。这段代码不会漏掉未能中断的情况,并将退出代码设为失败。由于实际时间会根据操作系统负载而波动,请同时对响应时间进行实际测量。
与 AbortSignal.timeout 的区别
在近年来的 JavaScript 环境中也有AbortSignal.timeout(1000)。
const response = await fetch("/api/data", {
signal: AbortSignal.timeout(1000)
});
这个示例也将在包含目标API的环境中使用。当超过一定时间后,AbortSignal.timeout通常会TimeoutError返回异常。另一方面,如果显式调用了controller.abort(),通常会变成AbortError。
请在发布时确认浏览器和Node.js的支持版本。特别是在需要兼容性的企业应用中,有时也需要面向旧环境。
发生错误时的处理方式
| 状态 | 常规处理方式 | 下一步确认 |
|---|---|---|
| AbortError | 由用户操作等引起的中断 | 显示中断操作的原因 |
| TimeoutError | 由时间流逝引起的中断 | 响应延迟与重试策略 |
| TypeError等 | 网络/其他获取失败 | 通信目标、权限、CORS、网络 |
| HTTP 500 | fetch本身有时可以解决 | 通过 response.ok / status 进行判断 |
特别重要的是,HTTP 500 不一定会使 fetch 的 Promise 变为 reject 状态。只要请求成功送达并获取到 HTTP 响应,HTTP 错误就可以response.status通过它来判断。因此,我们需要按如下方式进行确认:
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP status: ${response.status}`);
}
在实际业务中使用时
重试读取数据的 API 与重试下单、转账等更新类的 API,其风险程度是不同的。客户端中断了请求,并不意味着服务器上的处理也已被取消。
重新执行更新类的 API 需要设计幂等键、处理 ID、结果核对以及防重复机制等。AbortController 是取消网络通信的手段,而不是业务上的交易取消功能。
