Axios multipart/form-data 完整指南:FormData 自动序列化、formSerializer 配置与 formToJSON 反向转换
本文围绕 Axios 的 multipart/form-data 发送能力展开:从浏览器与 Node.js 环境下的基础提交,到 v0.27.0 引入的对象自动序列化机制、formSerializer 各配置项(dots、metaTokens、indexes、maxDepth 等)的取值与行为,再到 Node.js 下 formDataHeaderPolicy 头安全策略与 formToJSON() 的反向解析。读完你可以完整掌握 Axios 表单序列化的规则细节,并结合 toFormData.js、setFormDataHeaders.js、formDataToJSON.js 三处源码理解其底层实现。
基础提交:浏览器与 Node.js 两种写法
Axios 支持发送 multipart/form-data 格式的请求,该格式常用于文件上传场景。基本思路是:构造一个 FormData 对象并 append 数据,再将其作为 data 传给 axios 请求配置。
浏览器端直接使用原生 FormData:
const formData = new FormData();
formData.append('foo', 'bar');
axios.post('https://httpbin.org/post', formData);
不要手动设置 Content-Type 头。对于浏览器、Web Worker 或 React Native 环境中的 FormData 对象,环境本身会自动添加 multipart boundary(边界串),手动设置反而会破坏请求体格式。
在 Node.js 中通常使用社区 form-data 包(Axios 自身的 polyfill 也基于它):
const FormData = require('form-data');
const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', Buffer.alloc(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
axios.post('https://example.com', form);
自动序列化为 FormData(v0.27.0+)
自 v0.27.0 起,当请求的 Content-Type 头被设置为 multipart/form-data 时,Axios 会自动把普通 JavaScript 对象序列化为 FormData 对象——你无需手工 append 任何字段:
import axios from 'axios';
axios
.post(
'https://httpbin.org/post',
{ x: 1 },
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
.then(({ data }) => console.log(data));
在 Node.js 版本中默认使用 form-data polyfill;你也可以通过配置项 env.FormData 替换所用的 FormData 类,尽管大多数情况下没有必要:
const axios = require('axios');
var FormData = require('form-data');
axios
.post(
'https://httpbin.org/post',
{ x: 1, buf: Buffer.alloc(10) },
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
.then(({ data }) => console.log(data));
从源码看,这条“自动序列化”链路位于默认请求转换器 defaults/index.js:当 data 是对象且 Content-Type 包含 multipart/form-data(或 data 本身是 FileList)时,会调用 toFormData(),并从 this.env.FormData 中取出自定义类实例作为容器:
// lib/defaults/index.js
const formSerializer = own(this, 'formSerializer');
// ...
if (
(isFileList = utils.isFileList(data)) ||
contentType.indexOf('multipart/form-data') > -1
) {
const env = own(this, 'env');
const _FormData = env && env.FormData;
return toFormData(
isFileList ? { 'files[]': data } : data,
_FormData && new _FormData(),
formSerializer
);
}
两个值得注意的实现细节:其一,纯 FileList 会被包装成 { 'files[]': data } 后再序列化,因此文件批量上传默认使用 files[] 字段名;其二,transformRequest 里还有反向判断——如果 data 已经是 FormData 而 Content-Type 却是 JSON,会走 formDataToJSON(data) 再 JSON.stringify(见 defaults/index.js),即“FormData + JSON 头”会被自动转成 JSON 发送。
Node.js FormData 的头拷贝策略 formDataHeaderPolicy(仅 Node.js)
当传入一个暴露了 getHeaders() 方法的 Node.js FormData 对象(如 form-data 包)时,Axios 默认会把它返回的全部头拷贝到请求上。这一行为保持了与 v1 的兼容性,但当 FormData 来自不可信来源时存在隐患:getHeaders() 可能覆盖 Authorization 等关键头,或注入任意自定义头。
设置 formDataHeaderPolicy: 'content-only' 后,Axios 只从 getHeaders() 拷贝 Content-Type 和 Content-Length 两个头,其余请求头必须通过 headers 配置显式声明:
await axios.post('https://example.com/upload', form, {
formDataHeaderPolicy: 'content-only',
headers: {
Authorization: 'Bearer my-token',
},
});
该配置的默认值是 'legacy'(全部拷贝)。参数解析逻辑非常直白,见 setFormDataHeaders.js:
const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length'];
export default function setFormDataHeaders(headers, formHeaders, policy) {
if (policy !== 'content-only') {
headers.set(formHeaders);
return;
}
Object.entries(formHeaders || {}).forEach(([key, val]) => {
if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
headers.set(key, val);
}
});
}
调用点有两处:统一在 resolveConfig.js 中基于 getHeaders() 应用一次,Node 的 http 适配器 adapters/http.js 中再次应用以覆盖重定向等场景。更完整的配置说明可参考请求配置文档中的 formDataHeaderPolicy 小节(docs/fr/pages/advanced/request-config.md)。
特殊终结符:{} 与 []
Axios 的 FormData 序列化器支持两种字段名终结符,用于表达特殊序列化操作:
{}— 将该值用JSON.stringify序列化后作为单个字符串字段发送;[]— 把数组类型的对象拆解为多个同名字段发送。
注意:对数组和 FileList 类型,拆解(展开为同名字段)是默认行为,无需加 []。
配置序列化器:config.formSerializer
序列化器支持通过 config.formSerializer 配置对象传入额外选项,用于处理各种特殊需求:
| 选项 | 类型与默认值 | 说明 |
|---|---|---|
visitor |
Function |
用户自定义的访问器函数,会被递归调用以按自定义规则将数据对象序列化为 FormData |
dots |
boolean = false |
序列化数组和对象时使用点号记法(user.name)替代方括号记法(user[name]) |
metaTokens |
boolean = true |
在字段名中保留特殊终结符(如 user{}: '{"name": "John"}')。后端 body-parser 可利用这些元信息自动按 JSON 解析该值 |
indexes |
null | false | true = false |
控制为展开后的数组字段追加何种索引:null — 不加括号(arr: 1、arr: 2、arr: 3);false(默认)— 加空括号(arr[]: 1…);true — 加真实下标(arr[0]: 1、arr[1]: 2、arr[2]: 3) |
maxDepth |
number = 100 |
序列化递归的最大对象嵌套深度。超限抛出 code 为 ERR_FORM_DATA_DEPTH_EXCEEDED 的 AxiosError,用于防御服务端通过深层嵌套负载发起的 DoS 攻击;设为 Infinity 可关闭限制 |
Blob |
typeof Blob |
将 ArrayBuffer 值转换为符合规范的 FormData 时使用的 Blob 构造函数。仅在你的运行时以其他标识符提供了兼容 Blob 构造器时才需要替换 |
例如,对确实会超过 100 层嵌套的数据结构放宽限制:
// 允许更深的嵌套,用于确实超过 100 层的数据结构
axios.postForm('/api', data, { formSerializer: { maxDepth: 200 } });
安全说明:默认 100 层上限是有意为之。服务端把客户端可控的 JSON 透传给 axios 作为 data 的代码,缺少该保护时会面临调用栈溢出风险;仅在你的数据结构确实需要时才调高 maxDepth。
源码视角:序列化器内部如何工作
核心实现位于 toFormData.js。默认上限与反向转换共享同一个常量,保证 FormData 与 JSON 互转的深度语义对称(toFormData.js):
// lib/helpers/toFormData.js
export const DEFAULT_FORM_DATA_MAX_DEPTH = 100;
几个关键机制:
- 深度保护:
build()在每次递归前调用throwIfMaxDepthExceeded(depth)检查层级,超限即抛出带ERR_FORM_DATA_DEPTH_EXCEEDEDcode 的AxiosError(toFormData.js);对{}终结符触发的JSON.stringify也内置了等价的深度限制stringifyWithDepthLimit()。 - 循环引用检测:
build()维护一个stack,遇到重复对象立即抛出Circular reference detected错误,避免死递归(toFormData.js)。 indexes三分支:平铺数组展开时的字段名生成就写死在defaultVisitor中——indexes === true走renderKey([key], index, dots)生成arr[0],indexes === null直接用key,其余情况(默认false)拼key + '[]'(toFormData.js)。metaTokens:{}终结符在metaTokens为true时原样保留在字段名里,否则被key.slice(0, -2)剥掉(toFormData.js)。dots:字段路径渲染由renderKey(path, key, dots)完成,dots为真时用.连接,否则用方括号包裹(toFormData.js)。- 值类型转换:
Date转为 ISO 字符串、布尔转字符串、ArrayBuffer/TypedArray 在有规范兼容Blob时转 Blob、在 Node 有 Buffer 时转 Buffer(convertValue,toFormData.js)。 visitor扩展点:自定义 visitor 以 FormData 为this被调用,返回true表示继续递归遍历该值;同时可访问defaultVisitor、convertValue、isVisitable及一组is*类型判断辅助函数(exposedHelpers,toFormData.js)。
序列化过程完整示例
以一个典型的嵌套对象为例:
const obj = {
x: 1,
arr: [1, 2, 3],
arr2: [1, [2], 3],
users: [
{ name: 'Peter', surname: 'Griffin' },
{ name: 'Thomas', surname: 'Anderson' },
],
'obj2{}': [{ x: 1 }],
};
Axios 序列化器内部等价于执行了以下步骤:
const formData = new FormData();
formData.append('x', '1');
formData.append('arr[]', '1');
formData.append('arr[]', '2');
formData.append('arr[]', '3');
formData.append('arr2[0]', '1');
formData.append('arr2[1][0]', '2');
formData.append('arr2[2]', '3');
formData.append('users[0][name]', 'Peter');
formData.append('users[0][surname]', 'Griffin');
formData.append('users[1][name]', 'Thomas');
formData.append('users[1][surname]', 'Anderson');
formData.append('obj2{}', '[{"x":1}]');
规则可以归纳为:纯标量直接追加;无嵌套的平铺数组默认展开为 key[] 同名多值;混合数组按真实下标生成路径(arr2[1][0]);对象数组递归展开为 users[0][name] 这类路径;{} 终结符的值整体 JSON.stringify 成单字段。
反向转换:axios.formToJSON()
axios.formToJSON() 能把字段名中的点号与方括号记法还原为嵌套的对象和数组结构。只有 .、[、] 是结构性分隔符,其余字符(如 -、空格、+、*、&)都会原样保留在字面量键中:
const form = new FormData();
form.append('user-name', 'johndoe');
form.append('user.name', 'john');
console.log(axios.formToJSON(form));
// {
// 'user-name': 'johndoe',
// user: { name: 'john' }
// }
user[name] 同样会产生嵌套对象路径,items[] 则产生数组。
实现见 formDataToJSON.js,几个实现要点:
- 路径解析使用正则
/[^.[\]]+|\[([^.[\]]*)]/g,把foo[x][y][z]拆为['foo','x','y','z'],foo.x.y.z同理;段内不含[的设计让解析保持线性复杂度(formDataToJSON.js)。 __proto__段被直接跳过,杜绝原型污染;同名重复字段会合并为数组(formDataToJSON.js)。- 数字键(
0、1…)在目标位置自动形成数组,非数字键还原为对象属性;解析同样受 100 层深度上限保护,超限抛ERR_FORM_DATA_DEPTH_EXCEEDED。 - 该函数挂在 axios 实例上,且支持直接传 HTML
<form>元素——内部会先new FormData(thing)再解析(axios.js)。对应的单元测试见 tests/unit/helpers/formDataToJSON.test.js。
toFormData 本身也作为 axios.toFormData 暴露(axios.js),可以脱离请求流程手动序列化对象,测试用例见 tests/unit/toFormData.test.js。
便捷方法:postForm / putForm / patchForm
Axios 还提供三个快捷方法:postForm、putForm、patchForm。它们与对应的 HTTP 方法完全等价,唯一区别是预先将 Content-Type 头设置为 multipart/form-data,因此可以直接传普通对象:
// 等价于 axios.post('/api', data, { headers: { 'Content-Type': 'multipart/form-data' } })
axios.postForm('/api', { name: 'john', file: new Blob(['...']) });
这也意味着 postForm 系列方法天然触发前文描述的自动序列化流程,并受同一套 formSerializer 选项约束。
小结
- 浏览器 / Web Worker / React Native:用原生
FormData直接传data,不要手动设置Content-Type(boundary 由环境生成)。 - Node.js:使用
form-data包或依赖自动序列化;对不可信来源的 FormData,用formDataHeaderPolicy: 'content-only'收敛头拷贝范围。 - 对象自动序列化要求
Content-Type含multipart/form-data;formSerializer的visitor/dots/metaTokens/indexes/maxDepth/Blob覆盖几乎全部特殊场景,maxDepth默认 100 层是服务端 DoS 防护线。 - 终结符
{}(JSON 化)与[](字段展开)是字段级的序列化开关;formToJSON()按.、[、]三个结构性分隔符做无损逆向解析,并与序列化器共享同一深度上限。
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