Axios 请求取消机制详解:AbortController 与 CancelToken 的完整实践
从 v0.22.0 开始,Axios 支持通过 AbortController 以标准 Web 方式干净地取消请求;对于旧代码,仓库仍保留了已标记为废弃的 CancelToken API(将在下一个主版本中移除)。读完本文,你将掌握两种取消方式的完整用法、取消错误(CanceledError)的识别与类型收窄技巧,以及信号在 dispatchRequest 和各平台 adapter 中的实际接线方式——从浏览器 XHR 到 Node.js HTTP 适配器。
使用 AbortController 取消请求(推荐方式)
AbortController 是浏览器与 Node.js 的 Web 标准 API,Axios 通过请求配置的 signal 选项与其对接。基本用法是:创建一个 AbortController 实例,把它的 signal 传给请求,需要取消时调用 controller.abort():
const controller = new AbortController();
axios
.get('/foo/bar', {
signal: controller.signal,
})
.then(function (response) {
//...
});
// cancel the request
controller.abort();
这种方式的价值在于:AbortSignal 是跨生态的标准信号对象,可以同时在 fetch、WebSocket、Node 的 http 请求以及第三方库之间复用,无需绑定 Axios 私有的取消机制。从仓库源码可以看到,signal 会被两个层面消费:
- 调度前检查:dispatchRequest 中的
throwIfCancellationRequested(config)会在发出请求前检查config.signal.aborted,若已中止则直接抛出CanceledError,不产生真实网络请求:
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new CanceledError(null, config);
}
}
- adapter 运行时监听:在请求飞行期间,adapter 会订阅
abort事件。浏览器端 xhr.js 中可以看到对cancelToken和signal的双通道处理:
if (_config.cancelToken || _config.signal) {
// Add a cleanup callback to remove the abort event listener
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
if (_config.signal) {
_config.signal.aborted
? onCanceled()
: _config.signal.addEventListener('abort', onCanceled);
}
}
Node.js 端 http.js 采用了完全一致的模式,并在请求结束(onReqResponse 等路径)时通过 removeEventListener / unsubscribe 清理监听器,避免信号对象上的监听器泄漏。
此外,composeSignals 工具会把 timeout 产生的定时器信号与用户传入的 signal 合并为一个统一的 AbortController 信号供 adapter 内部使用——这意味着 timeout 与 signal 在底层走的是同一条中止链路:超时触发 AxiosError(ETIMEDOUT),外部取消则触发 CanceledError。
使用 CancelToken 取消请求(已废弃)
文档明确标注了 CancelToken 为 Obsoleto(已废弃):它仍可用,但将在下一个主版本中移除,新代码建议使用 AbortController。它有两种创建方式。
方式一:CancelToken.source() 工厂
通过静态方法 CancelToken.source() 创建一个 { token, cancel } 对,同一个 token 可以同时传给 GET 和 POST:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
.get('/user/12345', {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post(
'/user/12345',
{
name: 'new name',
},
{
cancelToken: source.token,
}
);
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
从 CancelToken.js 的源码看,source() 本质上是用一个闭包保存执行器注入的 cancel 函数:
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel,
};
}
方式二:通过执行器函数构造
向 CancelToken 构造函数传入执行器函数,函数参数即为取消函数,可在任意时机保存并调用:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
}),
});
// cancel the request
cancel();
构造函数的核心逻辑(CancelToken.js)值得注意三点:
- 执行器参数必须是函数,否则抛出
TypeError; - 内部持有一个
promise,执行器调用cancel(message, config, request)时,若尚未取消过(token.reason为空),会创建CanceledError并 resolve 该 promise,随后遍历通知所有已订阅的监听器;重复调用cancel会被忽略,保证取消只发生一次; - 被覆写的
promise.then实现了「链式订阅」:.then回调只会在取消时触发,且返回的 promise 带有cancel()方法用于退订。
CancelToken 的低层辅助 API(面向遗留集成)
除了面向请求的用法,CancelToken 还暴露了几个低层 helper,用于旧框架或自定义封装的集成场景(原文档给出的完整示例):
const source = axios.CancelToken.source();
const listener = (cancel) => {
console.log(cancel.message);
};
source.token.subscribe(listener);
const signal = source.token.toAbortSignal();
// Pasa `signal` a APIs que acepten AbortSignal.(把 signal 传给接受 AbortSignal 的 API)
source.cancel('Operation canceled by the user.');
source.token.unsubscribe(listener);
对应源码行为:
subscribe(listener)/unsubscribe(listener)(CancelToken.js):注册/移除取消监听器。若订阅时 token 已被取消,监听器会同步立即收到reason(即CanceledError),无需等待任何异步事件;toAbortSignal()(CancelToken.js):内部new AbortController()并把abort回调订阅到 token 上,取消时以controller.abort(err)携带原因中止。返回的signal额外挂了unsubscribe方法用于退订——这意味着废弃的 CancelToken 可以桥接到接受标准AbortSignal的第三方 API,是向AbortController世界迁移时的关键过渡工具;throwIfRequested()(CancelToken.js):若reason已存在则抛出它。dispatchRequest正是靠它在请求发出前完成「立即取消」的判定。
取消错误:CanceledError 与 isCancel
被取消的请求会以 axios.CanceledError 拒绝。查看 CanceledError.js 的实现:
class CanceledError extends AxiosError {
constructor(message, config, request) {
super(message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request);
this.name = 'CanceledError';
this.__CANCEL__ = true;
}
}
关键点:
- 它继承自
AxiosError,code为ERR_CANCELED,message缺省为'canceled'; - 实例上带有
__CANCEL__ = true标志,这是 isCancel 的判定依据——整个函数体只有一行:return !!(value && value.__CANCEL__);; - 遗留的
axios.Cancel导出是axios.CanceledError的别名,保证旧代码中instanceof axios.Cancel的写法不破裂。
仓库的测试用例(canceledError.test.js)验证了两条行为:toString() 在有无 message 时分别输出 CanceledError: canceled 与自定义消息;且 CanceledError 能被 Node 的 util.types.isNativeError 识别为原生错误,这在生产环境依赖堆栈/错误上报的场景中是有意义的。
TypeScript:isCancel 的类型收窄
在 TypeScript 中,isCancel<T, D, P>() 可以收窄 unknown 错误类型,同时保留响应数据、请求体与查询参数的泛型:
interface SearchResponse {
results: string[];
}
interface RequestBody {
includeArchived: boolean;
}
interface SearchParams {
query: string;
}
try {
await axios.get("/search");
} catch (error) {
if (axios.isCancel<SearchResponse, RequestBody, SearchParams>(error)) {
error.response?.data; // SearchResponse | undefined
error.config?.data; // RequestBody | undefined
error.config?.params; // SearchParams | undefined
}
}
这在处理「取消后仍需要读取已到达的部分响应」或审计取消原因(访问 error.config)时非常实用。
多请求共享 token 与「启动即取消」
你可以用同一个取消 token(或同一个 AbortController)取消多个请求。还有一个容易被忽略但重要的语义:如果 token 在 Axios 请求启动时就已经处于已取消状态,该请求会被立即取消,不会尝试发起任何真实请求。
这一点在源码中有明确落点:dispatchRequest 的第一步就是 throwIfCancellationRequested(config)(dispatchRequest.js),它先检查 cancelToken.throwIfRequested(),再检查 signal.aborted;即使请求已经发出,adapter 返回后的 onAdapterResolution / onAdapterRejection 路径里也会再次调用 throwIfCancellationRequested(dispatchRequest.js),确保「响应回来前一刻的取消」同样以 CanceledError 呈现,而不是让一个已废弃的响应被当作成功结果交付给业务代码。
迁移建议小结
结合文档标注与源码现状,实践上可以按以下优先级处理:
| 场景 | 推荐做法 |
|---|---|
| 新代码 / 新库集成 | 一律使用 AbortController + signal 选项 |
旧代码使用 cancelToken |
继续可用,但规划迁移;可用 token.toAbortSignal() 与标准 API 桥接 |
| 需要监听取消事件 | 迁移前用 token.subscribe(listener),迁移后改为 signal.addEventListener('abort', ...) |
| 错误分支判断 | 优先 axios.isCancel(err)(基于 __CANCEL__ 标志),TypeScript 中配合泛型 isCancel<T, D, P> 收窄类型 |
与 timeout 共存 |
无需额外处理:composeSignals 已在底层合并两者,取消报 CanceledError,超时报 AxiosError(ETIMEDOUT) |
相关实现与测试入口:CancelToken.js、CanceledError.js、isCancel.js、composeSignals.js、dispatchRequest.js,以及单元测试 canceledError.test.js 与 isCancel.test.js。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00