axios 开发者指南:从 AGENTS.md 看 axios 1.x 的架构边界、请求生命周期与安全约定
axios 是面向浏览器与 Node.js 的 Promise 风格 HTTP 客户端。本文围绕仓库中的贡献者指南 AGENTS.md(CLAUDE.md 仅是一行 @AGENTS.md 的引用桩,全部内容以 AGENTS.md 为准)展开,结合 lib/ 源码与 tests/ 目录逐项印证其中每一条约定。读完本文,你将掌握:如何在该仓库中安全地构建、跑单测与浏览器测试;axios 的请求分发全链路(配置合并、拦截器、适配器选择、数据变换)在源码中的真实调用位置;以及错误码、取消机制、命名规范与原型污染防护等“安全敏感代码”的具体实现细节。
1. 项目定位与文档入口
AGENTS.md 开篇说明:axios 的默认实例从 lib/axios.js 经 index.js 导出;浏览器构建使用 XHR 或 Fetch 适配器,Node 使用 HTTP/HTTPS 适配器,平台选择位于 lib/platform/ 目录。该文件被明确定位为“人类与 AI Agent 共同遵循的权威贡献指南”,而 .github/copilot-instructions.md 只是指回它的薄桩——修改承载重量的安全规则时需要同步二者。
从 lib/axios.js 源码可以印证这一点:createInstance(defaultConfig) 创建 Axios 实例并把 Axios.prototype.request 通过自定义 bind 绑定为 request 方法,随后挂上 AxiosError、CancelToken、isCancel、toFormData、AxiosHeaders、HttpStatusCode、getAdapter、mergeConfig 等静态成员;index.js 再把默认导出解包为一组命名导出(create、Axios、AxiosError、CanceledError……),保证 ESM 与 CJS 两种模块形态的顶层导出一致。
2. 环境搭建与安全红线(Setup And Safety)
指南的“Setup And Safety”一节是硬性规则,逐条与仓库事实核对:
- 必须使用
npm ci安装。仓库根目录的.npmrc只有一行:ignore-scripts=true,CI 同样使用npm ci --ignore-scripts。目的是不在安装阶段执行任何依赖包的 postinstall 脚本。 - 不要删除
ignore-scripts=true。若新安装后需要 git hooks,只需执行一次npm rebuild husky && npx husky恢复钩子。 - 依赖变更是安全敏感操作。package-lock.json 会被
lockfile-lint检查 npm HTTPS 主机与 integrity 哈希;包、锁文件、GitHub Actions 的更新 PR 仅限维护者/机器人,外部协作者提的此类 PR 应直接关闭;Dependabot 的 7 天延迟除非有严重漏洞否则保持不动。 - 不要在没有讨论的情况下新增运行时依赖,依赖面被刻意维持得很小。可以印证:package.json 的
dependencies仅有 4 个包——follow-redirects、form-data、https-proxy-agent、proxy-from-env,全部是 Node 侧适配器所需的网络能力。 - 注意一个隐含风险:即便设置了
ignore-scripts,构建/测试/lint 工具本身仍会执行依赖代码,所以指南建议“能聚焦验证就不要跑全量构建”。
3. 构建与测试命令全集
AGENTS.md 的 Commands 一节与 package.json 的 scripts 字段一一对应,此处完整继承并补充实际执行位置:
| 目的 | 命令 | 说明 |
|---|---|---|
| 构建发布产物 | npm run build |
实际为 gulp clear && cross-env NODE_ENV=production rollup -c,先清空 dist/ 再由 Rollup 输出浏览器 ESM/UMD/CJS 与 Node CJS 包 |
| 仅 lint 源码 | npm run lint |
等价于 eslint lib/**/*.js |
| 聚焦单文件 lint | npx eslint lib/path/to/file.js |
修改个别文件时的快速校验 |
| 单元测试 | npm run test:vitest:unit |
即 vitest run --project unit |
| 聚焦单测文件 | npm run test:vitest:unit -- tests/unit/path.test.js |
只跑指定用例 |
| 浏览器测试 | 先 npx playwright install(CI 用 --with-deps),再 npm run test:vitest:browser:headless |
对应 vitest run --project browser-headless |
几个值得注意的细节:
- 冒烟/模块兼容性套件测的是打包产物而非源码树。流程是:
npm run build→npm pack→ 把 tarball 装入对应tests/smoke/*或tests/module/*的独立包 → 运行该套件自己的 npm script。仓库提供了test:smoke:cjs:vitest、test:smoke:esm:vitest、test:smoke:deno、test:smoke:bun、test:module:cjs、test:module:esm六个入口脚本(见 package.json)。 - CI 顺序:install → build → Playwright install → unit → browser headless → pack → CJS/ESM module 与 smoke 测试 → Bun/Deno smoke 测试。
- 测试项目由 vitest.config.js 定义:
unit项目匹配tests/unit/**/*.test.js,browser与browser-headless项目匹配tests/browser/**/*.browser.test.js并加载tests/setup/browser.setup.js。
4. 包形态:入口、exports 与生成文件
“Package Shape”一节定义了包的分发结构,均可在 package.json 中逐条对上:
- 源码是 ESM(
"type": "module");公共 ESM 入口是index.js,重新导出lib/axios.js的默认实例。 - 不要手工编辑
dist/——它被 git 忽略,由 Rollup 从lib/生成。 - 运行时导出按环境切分(
exports字段):react-native与browser条件下把 Node 的 HTTP/平台文件映射到浏览器或 null 替代;Node CJS 走dist/node/axios.cjs(main字段)。browser字段同样把lib/adapters/http.js别名到lib/helpers/null.js、把lib/platform/node/index.js别名到lib/platform/browser/index.js——这正是指南所说“浏览器构建依靠 package/rollup 别名到lib/platform/browser”的落地方式。 - 公共运行时导出、
index.d.ts(ESM 类型)与index.d.cts(CJS 的export = axios类型)三者必须在 API 变更时同步修改。 lib/env/data.js由gulp version生成版本号,常规功能开发不应编辑它。
5. 架构边界:core / adapters / platform / helpers
指南用四条边界约束 lib/ 的分层,源码可以逐条验证:
lib/core/:axios 领域逻辑。 请求分发、配置合并、拦截器、头部、错误。关键类:
Axios(lib/core/Axios.js):request()入口 + 拦截器链装配;AxiosError(lib/core/AxiosError.js):标准化错误码体系;AxiosHeaders(lib/core/AxiosHeaders.js):大小写不敏感的头部归一化;InterceptorManager(lib/core/InterceptorManager.js):同步/异步拦截器注册。
lib/adapters/:执行 I/O。 默认适配器偏好顺序是 ['xhr', 'http', 'fetch'],在 lib/defaults/index.js 的 adapter 字段中声明,能力选择在 lib/adapters/adapters.js。该文件中的 getAdapter(adapters, config) 按顺序遍历候选:字符串名从 knownAdapters(http/xhr/fetch)查表,函数则直接可用;fetch 通过惰性 get(config) 获取,不可用的候选会记录拒绝原因,全部失败时抛出 AxiosError('There is no suitable adapter ...', ERR_NOT_SUPPORT) 并附带每个候选的具体原因。指南强调“按能力探测,而不是按环境名判断”。
lib/platform/:默认选 Node。 lib/platform/index.js 直接 import platform from './node/index.js' 并与 common/utils.js 合并导出;浏览器产物靠上文的包别名机制换到 lib/platform/browser。
lib/helpers/:保持通用。 应能在 axios 之外复用,禁止放入 axios 特定的请求生命周期逻辑。
代码风格约定:新增 lib/**/*.js 应使用显式 .js 扩展名的 ESM import、与现有库文件一致的 'use strict';,以及用 AxiosError 表达 axios 自身发起的失败。
6. 请求生命周期:九步链路在源码中的位置
指南给出的九步请求生命周期,可以在 lib/core/Axios.js 与 lib/core/dispatchRequest.js 中逐环节定位:
- 用户调用
axios()或方法别名。别名在Axios类底部生成:delete/get/head/options一组无 body 方法,post/put/patch/query一组带 body 方法(每个还附带xxxForm的 multipart 快捷形式),全部收敛到this.request(mergeConfig(...))。 - 配置合并:
_request()中config = mergeConfig(this.defaults, config),随后对transitional、paramsSerializer做validator.assertOptions校验——对应指南“通过validatorhelper 校验配置项,不要自造校验路径”。 - 请求拦截器执行(顺序见下一节),在
_request中装配成requestInterceptorChain。 - 适配器选择:进入 lib/core/dispatchRequest.js 后,先
throwIfCancellationRequested(config),再adapters.getAdapter(config.adapter || defaults.adapter, config)。 transformRequest:config.data = transformData.call(config, config.transformRequest);对post/put/patch补application/x-www-form-urlencoded缺省 Content-Type。- 适配器执行 HTTP 请求:
adapter(config).then(...)。 transformResponse:适配器 resolve 后执行transformData.call(config, config.transformResponse, response),并delete config.response清理暂存引用;reject 分支同样对reason.response做变换。- 响应拦截器执行。
- resolve 为
AxiosResponse或 reject 为AxiosError;取消(isCancel)时跳过响应变换直接透传。
另外注意 dispatchRequest 开头的 utils.toSafeFlatObject(_config)——这是把可能被拦截器替换的普通对象压平、防止共享原型成员变成请求行为的入口防护,与第 11 节的安全主题呼应。
7. 拦截器执行顺序:文档规则与 legacy 开关
指南明确:
- 请求拦截器后注册先执行(LIFO);
- 响应拦截器先注册先执行(FIFO);
- 二者都支持
synchronous: true(链中没有异步处理器时避免 Promise 包装)与runWhen: (config) => boolean条件执行; - 顺序对行为与测试都重要,新增内置拦截器时必须记录顺序。
阅读 lib/core/Axios.js 的 _request 后,可以补充一个源码级细节:实际走向由 config.transitional.legacyInterceptorReqResOrdering 决定。该标志为真(默认值,见 lib/defaults/transitional.js 中 legacyInterceptorReqResOrdering: true)时,请求拦截器通过 unshift 前置、响应拦截器经 push 后接,最终呈现“请求 FIFO、响应 LIFO”的 v1 旧行为;置为 false 后,requestInterceptorChain 与 responseInterceptorChain 直接拼接进 [dispatchRequest.bind(this), undefined] 的 Promise 链,才得到指南描述的“请求 LIFO、响应 FIFO”的现代化顺序。官方文档 docs/pages/advanced/interceptors.md 中“拦截器执行顺序”一节描述的是后者的现代语义。也就是说:指南约定的是目标行为,而当前默认配置保留了对旧行为的兼容开关,修改拦截器相关代码时必须同时理解这两个分支。
8. 错误处理:AxiosError 体系与错误码清单
指南要求:axios 自身发起的失败一律抛 AxiosError 而非裸 Error,并传齐 (message, code, config, request, response) 五参;第三方错误用 AxiosError.from(error, code, config, request, response) 包装。lib/core/AxiosError.js 完全对应:
- 构造函数签名正是
constructor(message, code, config, request, response),设置isAxiosError = true,并从response.status提升status; - 静态
from()保留原错误为不可枚举的cause(避免结构化日志遇到循环引用),聚合 NodeAggregateError的空 message,并在原错误带status时补全。
规范错误码清单在 AxiosError 静态属性上定义(lib/core/AxiosError.js 第 204–217 行):
ERR_BAD_OPTION_VALUE、ERR_BAD_OPTION、ECONNABORTED、ETIMEDOUT、ECONNREFUSED、ERR_NETWORK、ERR_FR_TOO_MANY_REDIRECTS、ERR_DEPRECATED、ERR_BAD_RESPONSE、ERR_BAD_REQUEST、ERR_CANCELED、ERR_NOT_SUPPORT、ERR_INVALID_URL、ERR_FORM_DATA_DEPTH_EXCEEDED。
另外,源码中还实现了指南未展开的 redact 机制:AxiosError.toJSON() 会在 config 携带 redact 数组时,把大小写不敏感的匹配键值(任意深度,含数组与 AxiosHeaders)替换为 [REDACTED ****],序列化 config 快照时防止凭据泄漏——这是“错误对象可被安全地 JSON 化”的补充实现事实。
9. 取消机制:CancelToken 与 AbortSignal 并存
指南对取消的三条要求:
CancelToken(旧)与AbortSignal(新)同时支持,不得破坏任一路径;- 取消必须在生命周期的任何阶段生效,包括正在读取响应体途中;
- settle 或取消后必须移除 signal 监听器,防止内存泄漏。
源码印证:lib/cancel/CancelToken.js 与 lib/cancel/CanceledError.js 承载旧 API;lib/core/dispatchRequest.js 的 throwIfCancellationRequested 同时检查 config.cancelToken.throwIfRequested() 与 config.signal.aborted,在请求发出前、适配器 resolve 后、reject 分支各检查一次,覆盖“发出前取消”与“响应已收到才取消”两种窗口。Axios.js 顶部导入了 CanceledError,lib/axios.js 将其作为 axios.CanceledError(并保留 axios.Cancel 别名)暴露给使用者。
10. 命名规范与内部槽位
- 类:PascalCase(
Axios、AxiosError、InterceptorManager);函数:camelCase(buildURL、mergeConfig、dispatchRequest);错误码:UPPER_SNAKE_CASE 常量挂在AxiosError上。 - 内部类槽位使用 Symbol 键而非下划线前缀属性。lib/core/InterceptorManager.js 第 3 行即是:
const $internals = Symbol('internals'),用于缓存 handlers 快照与已取消句柄索引;lib/core/AxiosHeaders.js 采用同样手法。好处是内部状态不会与用户经utils.extend(instance, context, ...)复制来的公开配置属性冲突。
11. 常见陷阱(Common Pitfalls)
指南列了四条“不要做”,每一条都能在源码中找到对应的工程理由:
- 不要原地修改 config 对象,合并/变换必须返回新对象——lib/core/mergeConfig.js 的
mergeMap机制本身就产出新对象。 - 不要假设浏览器或 Node 专属全局量存在,先做能力检查——适配器选择与平台抽象(
lib/platform/)就是这个原则的体现。 - 不要直接用
Function.prototype.bind,要用 lib/helpers/bind.js:
export default function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
};
}
它通过 apply 转发 arguments,是库内(如 lib/axios.js 绑定 request)依赖的实现。
4. 不要从库代码抛裸 Error,用带 code 的 AxiosError(见第 8 节)。
12. 测试组织方式
指南的测试约定与 tests/ 目录结构完全一致:
- 运行时优先的布局:
tests/unit/**/*.test.js(Vitest unit 项目)、tests/browser/**/*.browser.test.js(浏览器项目)、tests/smoke/esm/**/*.smoke.test.js、tests/smoke/cjs/**/*.smoke.test.cjs,另见 tests/README.md。 - 本地 HTTP 服务统一使用 tests/setup/server.js,并在
try/finally中清理——泄漏的 server 会导致 Vitest 挂起。 - 打包/导入相关行为变更时,保持 CJS 与 ESM 冒烟覆盖对齐(两目录各有 auth、basic、cancel、error、fetch、files、formData、headers、http2、instance、interceptors、progress、rateLimit、timeout、urlencode 等成套用例)。
- 类型兼容性双版本验证:
tests/module/cjs用 TypeScript 4.9、tests/module/esm用 TypeScript 5.x;修改类型声明文件时运行对应套件。 - 浏览器测试会替换 XHR 等全局对象,清理钩子里必须恢复全局并重置 spy(配合 tests/setup/browser.setup.js)。
13. 安全敏感代码:原型污染与越权读取防护
这是指南中“承载重量”的安全规则,源码证据充分:
- 禁止对不受信 config 做原型链遍历读取(
in、解构、直接config.foo),必须用自有属性守卫。lib/utils.js 提供hasOwnProp(Object.prototype.hasOwnProperty.call的别名),lib/defaults/index.js 第 11 行的本地own(obj, key)helper 即基于它实现;lib/core/mergeConfig.js 全程使用utils.hasOwnProp逐键检查,并主动把hasOwnProperty恢复为不可枚举自有槽位以防被用户 config 污染。 - 新的合并/对象物化代码必须继续过滤
__proto__、constructor、prototype。lib/utils.js 第 20 行定义了isKeyFilter对这三个键的判断,第 586 行起的合并路径显式跳过;lib/core/mergeConfig.js 第 153 行同样有if (prop === '__proto__' || ...) return;。指南明确:这里的回归属于安全 bug。 - 触及 URL 构造、重定向、代理/环境变量处理、XSRF、socket 路径、解压上限或适配器的改动,应参考 THREATMODEL.md 并新增聚焦回归测试(仓库另有 tests/unit/prototypePollution.test.js 专门回归原型污染)。
withXSRFToken跨域行为保持显式:只有取值为true才强制附加跨域 XSRF 头。- 不要在没有覆盖凭据泄漏或 SSRF 类场景的测试时弱化
beforeRedirect、代理、socketPath的防护。
14. 预发布期的文档纪律
最后,“Pre-Release Notes”一节规定了未发布期间的文档流向:
- 用户可见的未发布变更记入 PRE_RELEASE_CHANGELOG.md,而不是 CHANGELOG.md——后者归发布流程所有,只在真正准备发版时更新;
- 延迟处理的 README、文档站、示例、迁移指南与翻译文档更新统一记录在 PRE_RELEASE_DOCS.md,要求提供足够上下文供发布准备使用,而不是存脆弱的 diff 或行号笔记;
- 除非任务明确是发布准备,否则不要为未发布的运行时/API 变更去改 README.md 或文档站;功能/修复开发期间,把“文档该写什么”记进
PRE_RELEASE_DOCS.md,留待发布工作一并应用。
结语
AGENTS.md 的价值在于把“构建命令、架构边界、拦截器顺序、错误规范、安全红线”收敛成一份与源码一一对应的可执行契约:npm ci + ignore-scripts 守住安装安全,gulp/rollup + vitest + playwright 定义了从单测到多运行时冒烟的验证链路,lib/core → lib/adapters → lib/platform 的分层约束了代码归属,而 hasOwnProp、__proto__ 过滤与双取消 API 则划定了安全边界。对贡献者或在本仓库工作的 AI Agent 而言,逐条对照本文所列源码文件验证规则,是提交任何改动前成本最低的正确性检查。
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 StartedRust0626
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