axios 预发布版本变更深度解析:NO_PROXY CIDR 匹配、HttpStatusCode 新命名与运行时配置加固
本文基于当前仓库中的 PRE_RELEASE_CHANGELOG.md 展开,逐条解读该预发布版本中的 2 项新特性、7 项 Bug 修复与文档改进,并结合 lib/helpers/shouldBypassProxy.js、lib/adapters/fetch.js、lib/core/InterceptorManager.js 等源码实现,帮助读者理解这些变更的设计动机、行为边界与升级注意事项。
变更总览
当前 PRE_RELEASE_CHANGELOG.md 的 Unreleased 段落包含三部分:
| 类别 | 数量 | 涉及能力 |
|---|---|---|
| Features | 2 | NO_PROXY/no_proxy CIDR 匹配、RFC 9110 状态码新命名 |
| Bug Fixes | 7 | 运行时配置加固、Fetch 适配器一致性、HTTP/2 适配器一致性、Data URI 校验、keep-alive 内存留存、XHR 导航取消、请求错误栈、拦截器存储(共 8 条修复项) |
| Documentation | 1 | 文档站浏览器内全文搜索 |
其中多数修复项带有 PR/Issue 编号(如 #11082、#11091、#11094、#11109、#11070),说明这些变更都经过了完整的缺陷报告—修复流程。下文按"新特性 → 适配器一致性修复 → 运行时安全加固 → 文档改进"的顺序逐条解析。
新特性一:NO_PROXY / no_proxy 支持 CIDR 网段匹配
这是本预发布版本最核心的新特性:为 NO_PROXY/no_proxy 环境变量新增 IPv4 与 IPv6 的 CIDR 网段匹配能力,包括带方括号的 IPv6 地址、IPv4-mapped IPv6 归一化,且格式错误的网段条目采用 fail-closed(拒绝匹配)策略。特别地,/0 前缀条目会对其整个地址族(AF 家族)内的所有地址生效,即 0.0.0.0/0 或 ::/0 会绕过该协议族的全部代理。
实现位置:lib/helpers/shouldBypassProxy.js
该能力完整落在 lib/helpers/shouldBypassProxy.js 中,主入口为 shouldBypassProxy(location)。结合源码可以看到几条关键设计:
- 环境变量读取与通配符:函数先读取
process.env.no_proxy或process.env.NO_PROXY并转小写(见 shouldBypassProxy.js#L447);整体为*时对所有请求放行,条目级*同样放行。 - CIDR 条目解析:
parseCidrEntry通过正则^(.+)\/(0|[1-9]\d{0,2})$识别形如192.168.0.0/16、[fd00::]/64的条目(见 shouldBypassProxy.js#L377-L414)。前缀为0的条目对应 changelog 中"/0条目绕过整个地址族"的语义;IPv4-mapped IPv6 基址若前缀小于 96 会被拒绝,否则按prefix -= 96折算到 IPv4 侧比较。 - 地址归一化:
normalizeIPAddress会把点分十进制的简写、八进制、十六进制形式(如127.0x00ff、127.65535)规范化为规范点分十进制;unmapIPv4MappedIPv6则把::ffff:192.168.1.5这类映射地址还原为192.168.1.5(见 shouldBypassProxy.js#L236-L260)。这正是 changelog 中"bracketed IPv6 and IPv4-mapped IPv6 normalization"的落地:不做归一化的话,NO_PROXY=192.168.1.5将无法匹配请求http://[::ffff:192.168.1.5]/,等于给代理绕过策略开了后门。 - 子网判定:
isInSubnet按整字节 + 剩余位掩码逐位比较(见 shouldBypassProxy.js#L416-L436),支持任意前缀长度。 - Fail-closed 策略:源码注释多处强调"fall through to non-bypass",即解析失败(空八位组、非法字符、前缀越界、非规范括号等)一律不匹配、继续走代理。这与 changelog 中"malformed ranges fail closed"的表述一一对应——安全相关的匹配逻辑宁可漏配也不误配。
使用示例
配置示例(Node 环境):
# 整个内网网段绕过代理
export NO_PROXY="192.168.0.0/16,10.0.0.0/8"
# IPv6 网段(方括号写法)
export NO_PROXY="[fd00:1234::]/48"
# 0.0.0.0/0 绕过所有 IPv4 流量(/0 对整个地址族生效)
export NO_PROXY="0.0.0.0/0"
除 CIDR 外,原有的主机名后缀(.example.com)、通配(*example.com)、精确主机与端口(host:8080)以及方括号 IPv6 端口写法 [::1]:8443 等匹配规则依然保留(见 shouldBypassProxy.js#L483-L504),对应测试文件为 tests/unit/helpers/shouldBypassProxy.test.js。
新特性二:HttpStatusCode 引入 RFC 9110 新命名
changelog 说明:为 413 与 422 增加 RFC 9110 官方名称 HttpStatusCode.ContentTooLarge 与 HttpStatusCode.UnprocessableContent,覆盖运行时 API 与 ESM/CommonJS 类型声明;旧名 PayloadTooLarge、UnprocessableEntity 保留为弃用别名;数字反查(HttpStatusCode[413])仍返回 v1.x 的既有名称以保持兼容(关联 #11082,关闭 #11066)。
对照 lib/helpers/HttpStatusCode.js 可直接验证:
/**
* @deprecated Use `ContentTooLarge` instead.
*/
PayloadTooLarge: 413,
ContentTooLarge: 413,
// ...
/**
* @deprecated Use `UnprocessableContent` instead.
*/
UnprocessableEntity: 422,
UnprocessableContent: 422,
文件末尾的反查表构建逻辑解释了"数字反查保留旧名"的实现原理:
Object.entries(HttpStatusCode).forEach(([key, value]) => {
if (HttpStatusCode[value] === undefined) {
HttpStatusCode[value] = key;
}
});
(见 HttpStatusCode.js#L82-L86)由于 Object.entries 保持声明顺序,413 先被 PayloadTooLarge 占用,后声明的 ContentTooLarge 不会覆盖它;422 同理仍反查到 UnprocessableEntity。这意味着按名字取值可以用新名称,按数字反查字符串的输出在 v1.x 中保持字节级一致,不会破坏依赖 HttpStatusCode[response.status] 做日志/告警分类的下游代码。相关测试见 tests/unit/helpers/HttpStatusCode.test.js。
Bug 修复一:运行时配置加固(防原型链污染)
这是本批次中篇幅最长、影响面最广的一条修复:阻止那些"仅从共享原型继承"的值(包括来自另一个 JavaScript realm 的 Object.prototype、甚至其 constructor 被篡改过的情形)在配置合并或拦截器替换之后变成请求行为。被保护的字段包括 methods、headers、adapters、transports、FormData hooks、序列化器选项以及 Fetch 的 Request 选项。changelog 进一步给出了细粒度策略:
- 已经"安全可写"的合并配置在 dispatch 过程中保持自身对象身份不变;
- 被
Object.freeze/Object.seal、基于访问器(accessor)受限、携带其他受限属性、或带"不安全键"的替换配置,会被转换为可写的过滤快照(writable filtered snapshot); - 应用自定义原型(非终端)的拦截器替换,以"归一化后的 null 原型快照"继续受支持;终端 null 原型祖先被视为共享边界,按 fail-closed 处理;
- 物化(materialized)配置中,自身的
__proto__、constructor、prototype键始终被排除。
从源码结构看,这一批加固与 lib/utils.js 中大量 hasOwnProp(仅识别自身属性)的访问模式、lib/core/mergeConfig.js 的合并语义、以及 lib/adapters/fetch.js 中对 config 取值时普遍使用 own('auth') 这类自身属性读取的写法一致(见 fetch.js#L226):整条链路都在刻意区分"配置对象自身的属性"与"从原型链继承的属性",后者在跨 realm(如 vm 沙箱、iframe、Worker)场景下可能是攻击者注入行为的路径。该修复的核心价值在于:即使攻击者能替换 config 或其拦截器传入的对象的 Object.prototype,也无法借此改变请求方法、注入请求头、替换适配器或改写序列化行为。
Bug 修复二:Fetch 适配器与自定义 fetch 实现的参数一致性
这条修复保证自定义 fetch 实现(通过 config.fetchOptions / 环境注入的 fetch)拿到的是"完整解析后的 Request + 一个安全的第二参数 fetchOptions"。行为要点:
- 第二参数保留了调用方的自定义自有字段(custom own fields),但不会覆盖 Axios 管理下的请求字段;
- 权威的 method、headers、body、signal、duplex、credentials 只存在于
Request上,并从第二参数中剔除; - 不支持
Request构造函数的降级路径(no-Requestfallback)拿到同样的安全解析选项; maxRedirects: 0表示"手动处理重定向":Node 侧会暴露未被跟随的 3xx 响应;浏览器侧可能暴露 status 为 0、headers 不可访问的 opaque redirect。
lib/adapters/fetch.js 中的实现与 changelog 描述精确对应:
const safeFetchOptions =
fetchOptions == null ? fetchOptions : Object.assign(Object.create(null), fetchOptions);
if (safeFetchOptions) {
// These options are owned by Axios and are already reflected in the
// resolved Request passed to fetch.
delete safeFetchOptions.body;
delete safeFetchOptions.headers;
delete safeFetchOptions.method;
delete safeFetchOptions.signal;
delete safeFetchOptions.duplex;
delete safeFetchOptions.credentials;
}
(见 fetch.js#L439-L451)随后:
if (maxRedirects === 0) {
resolvedOptions.redirect = 'manual';
if (safeFetchOptions) {
safeFetchOptions.redirect = 'manual';
}
}
request = isRequestSupported && new Request(url, resolvedOptions);
let response = await (isRequestSupported
? _fetch(request, safeFetchOptions)
: _fetch(url, resolvedOptions));
这里用 Object.create(null) 创建第二参数本身就值得注意:null 原型对象切断了原型链,避免用户传入的 options 对象经由原型携带 body/headers 等字段绕过上面的 delete 清理。对依赖自定义 fetch(如 undici 封装、mock、测试替身)的项目而言,本次修复后第二参数只可能携带你自己的字段,不会再出现"Axios 字段与 fetchOptions 字段谁说了算"的歧义。
Bug 修复三:HTTP/2 适配器一致性
针对 lib/adapters 目录下的 HTTP/2 支持(经由 http2 模块与 lib/helpers/Http2Sessions.js 的会话池),本批次修复了四类行为:
- 自定义 DNS lookup 生效:用户提供的
lookup函数现在会作用于 HTTP/2 连接,且可以安全地复用池中会话; - 显式代理对象的明确报错:直接传
proxy对象配置 HTTP/2 请求会抛出ERR_NOT_SUPPORT(ERR_NOT_SUPPORT常量定义见 lib/core/AxiosError.js#L215,适配器层的使用示例见 lib/adapters/adapters.js#L110); - 进程环境变量代理与 HTTP/1 agent 的
proxyEnv被显式忽略:原因是 Node 的http2.connect()本身无法应用 HTTP/1 风格的代理机制;proxy: false依然强制直连; - 失败会话清理:会话失败后从池中移除,不再泄漏未处理的 session 错误事件。
第 2、3 点实际上是在"明确边界":与其让代理配置在 HTTP/2 路径下静默失效(产生难以排查的"为什么没走代理"问题),不如显式报错或文档化忽略,这对依赖 docs/pages/advanced/http2.md 与 docs/pages/advanced/adapters.md 的读者是有用的行为契约。
Bug 修复四:Data URI 媒体类型校验加固
changelog 指出:现在会拒绝携带多余斜杠分隔符的媒体类型,并避免对超长畸形媒体类型发生正则回溯放大(excessive backtracking)。Data URI 的解析入口在 lib/helpers/fromDataURI.js,fetch 适配器在处理 data: URL 时(如 maxContentLength 前置校验,见 fetch.js#L303-L316)会与该辅助模块协作。
这条修复的安全意义在于两点:其一,data:text//html/... 这类畸形 media type 不再被"宽松解析"为可执行的类型判定;其二,拒绝带量词陷阱的正则写法,避免攻击者用构造的超长 data: 字符串触发 ReDoS,导致解析请求的路径挂起。对应测试见 tests/unit/fromDataURI.test.js 与 tests/unit/estimateDataURLDecodedBytes.test.js。
Bug 修复五:Node HTTP 适配器 keep-alive 内存留存
changelog 描述:复用了一个模块作用域的 socket 错误监听器,使池化的 keep-alive socket 不再持有"第一个使用该 socket 的请求"的适配器上下文、响应体与缓冲区;socket 错误依旧只会销毁当前活跃请求(#11091)。
从源码结构看,lib/adapters/http.js 中 socket 错误处理采用模块级共享监听器(socket.on('error', handleSocketError),见 http.js#L1342),并通过当前请求的引用标记(kAxiosCurrentReq 相关注释,见 http.js#L66 与 http.js#L1327 附近注释)区分 socket 归还到连接池后的错误归属。此前的实现会为每个 socket 挂一个捕获闭包上下文(含响应 body)的监听器,导致长驻 keep-alive 连接持续钉住旧请求的内存,表现为服务端内存随请求数缓慢爬升且 GC 无法回收。本次修复后,长连接池在高压场景下的内存占用将不再随历史请求累积,这对使用默认 http adapter 的 Node 服务是可感知的运维级改进。
Bug 修复六:XHR 适配器对"导航取消"请求的处理
这条修复针对浏览器 XHR 路径(lib/adapters/xhr.js):status 为 0 且到达 loadend 的响应,现在会以 ECONNABORTED 错误拒绝,而不是以空 body 解析为成功(#11094,关闭 #11093)。
changelog 给出的浏览器行为细节非常具体:Firefox 152 起,文档导航导致的请求取消不再触发 error 与 abort 事件,loadend 成为唯一执行的处理器,于是这类请求曾被 settle 为成功响应。修复后的行为对齐了 Firefox 151 时代 onabort 的报错语义,应用侧观察到的结果与浏览器更新前后保持一致。两个边界值得注意:
file:协议豁免:某些环境下file:读取成功也报告 status 0,因此仍按成功解析——无论 scheme 出现在请求 URL 上、相对 URL 从file:页面来源继承,还是仅出现在responseURL中;validateStatus不适用:由于根本没有收到响应,该拒绝不受validateStatus抑制,这与onerror/onabort的既有行为一致。
对依赖 XHR adapter 的前端项目,这条修复消除了"页面跳转取消的旧请求竟返回成功"的隐患(例如全局响应拦截器里对空成功响应的误处理)。
Bug 修复七:请求错误栈保留 与 拦截器存储墓碑修剪
请求错误栈:当自定义 Error 栈插桩(stack instrumentation,如某些 APM/覆盖率工具会修改 Error.prepareStackTrace)返回非字符串或在可选的栈装饰过程中抛出时,原始请求失败信息得以保留而不会被二次错误吞没(#11109,关闭 #11108)。这使得"自定义错误栈工具 + axios"组合在异常路径下不再丢失根因。
拦截器存储:lib/core/InterceptorManager.js 中 eject 采用"墓碑(tombstone)"机制——删除时不直接移位,而是置 null 占位以维持 ID 与索引稳定:
this.handlers[entry.index] = null;
// Do not reuse an index while forEach is walking its length snapshot.
if (!internals.iterationDepth) {
trimHandlers(this.handlers);
internals.handlersLength = this.handlers.length;
}
(见 InterceptorManager.js#L101-L124)配合 trimHandlers(见 InterceptorManager.js#L14-L22),本次修复(#11070)确保迭代结束后尾部的连续墓碑会被弹出:反复"注册—注销"同一位置拦截器不再让 handlers 数组无限增长,同时保持对外可见的数组形状、迭代行为(forEach 跳过 null)与 ID 身份语义不变。这是一个典型的可观测性修复——修复前长期运行的进程里 instance.interceptors.request.handlers.length 会随业务增长虚高。
文档改进:文档站浏览器内全文搜索
changelog 的 Documentation 部分:为当前文档语言新增了本地化、私有、纯浏览器内的全文搜索,支持模糊匹配与前缀匹配(fuzzy and prefix matching)。
"private"(私有)是关键词:搜索索引与查询全部在用户浏览器内完成,不经过任何第三方搜索服务;"localized"则意味着 docs/zh、docs/es、docs/fr 各语言站点分别建立索引,不会出现跨语言混检。实现侧可对照 docs/package.json 中文档站依赖与 docs/scripts 下的构建脚本;功能体验入口即文档站站点内的搜索框。
升级与验证建议
NO_PROXY使用者:升级前先在预发环境验证NO_PROXY条目——尤其是 CIDR 网段与 IPv6 方括号写法,确认"应直连"的地址确实直连;畸形条目会 fail-closed(继续走代理),与部分工具的"宽松解析"习惯不同。- 自定义 fetch 实现的项目:检查自定义 fetch 包装是否依赖从第二参数读取
method/headers/body等 Axios 已管理的字段——升级后这些字段只存在于Request上。 - 长期运行的 Node 服务:关注 keep-alive 连接池的内存曲线,本次修复预期可消除历史请求对象无法回收的问题。
- 依赖
HttpStatusCode数字反查的日志系统:无需变更,反查结果保持 v1.x 值;仅在新增代码中改用ContentTooLarge/UnprocessableContent命名即可。 - 验证途径:可运行 tests/unit 下的针对性测试(如 shouldBypassProxy.test.js、HttpStatusCode.test.js、InterceptorManager.test.js)与 tests/smoke 多运行时冒烟测试(cjs/esm/deno/bun)来确认本地构建行为与上述条目一致;测试整体说明见 tests/README.md。
小结
本预发布批次的变更呈现出清晰的三条主线:网络语义的严谨化(CIDR 代理绕过、Data URI 校验、HTTP/2 代理边界)、多实现/多浏览器行为的一致性(fetch 双参数契约、XHR 导航取消、状态码命名)、以及运行时健壮性与内存卫生(原型链污染加固、keep-alive 留存、拦截器墓碑修剪、错误栈保留)。对使用者而言,除 NO_PROXY CIDR 与 maxRedirects: 0 两项是显式新增能力外,其余变更均为行为修复且向后兼容——数字反查的状态码名称、拦截器 ID 语义、validateStatus 交互等公开契约均保持不变,可以放心跟进升级。
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