axios 中 multipart/form-data 上传实战:手动 FormData、自动序列化与 formSerializer 深度解析
axios 支持以 multipart/form-data 格式发送请求,这是文件上传场景最常用的格式。本文覆盖从"手动构造 FormData"到"对象自动序列化"的完整链路:浏览器与 Node.js 环境下 FormData 的构造差异、formDataHeaderPolicy 请求头安全策略、{} 与 [] 特殊键名后缀的行为规则、config.formSerializer 的六项序列化选项,以及 formToJSON 的反向转换能力,并结合 lib/helpers/toFormData.js、lib/core/setFormDataHeaders.js 等源码实现逐层印证。
手动发送 FormData 请求
发送 multipart/form-data 请求的基本方式是:创建一个 FormData 对象、向其追加数据、然后将其作为 data 传给 axios。
浏览器环境
const formData = new FormData();
formData.append('foo', 'bar');
axios.post('https://httpbin.org/post', formData);
重要约束:在浏览器、Web Worker 或 React Native 中,不要手动为 FormData 设置 Content-Type 头——这些环境会自行添加 multipart boundary。这一点在源码中有直接体现:resolveConfig.js 中,当检测到运行于标准浏览器、Web Worker 或 React Native 环境且 data 是 FormData 时,axios 会执行 headers.setContentType(undefined),把 Content-Type 的确定权交还给底层 fetch/XHR 实现。
Node.js 环境
Node.js 没有原生可用的 form-data 包,官方文档推荐使用 form-data 库(注意此为 npm 包名引用):
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);
Node.js 路径下 axios 与 form-data 包的关键协作点在 resolveConfig.js:只要传入的 FormData 对象暴露了 getHeaders() 方法(form-data 包的典型特征),axios 就会读取其生成的请求头并合并到请求上。
自动序列化为 FormData(v0.27.0+)
自 v0.27.0 起,axios 支持将普通 JS 对象自动序列化为 FormData:只要请求头 Content-Type 设为 multipart/form-data,直接把对象传给 data 即可。
import axios from 'axios';
axios
.post(
'https://httpbin.org/post',
{ x: 1 },
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
)
.then(({ data }) => console.log(data));
这个判断发生在默认 transformRequest 中。defaults/index.js 显示:当请求体是对象、且 Content-Type 包含 multipart/form-data(或 data 本身是 FileList 时,会被包装为 { 'files[]': data } 强制走 FormData 路径)时,axios 调用 toFormData(data, _FormData && new _FormData(), formSerializer) 完成转换。
在 Node.js 中,默认使用的 polyfill 就是 form-data 包。可以通过配置项 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));
env.FormData 的生效点在 defaults/index.js:const _FormData = env && env.FormData,若非空则以 new _FormData() 作为序列化目标容器。绝大多数场景无需覆盖,该选项主要服务于自定义 FormData 实现或测试替换。
formDataHeaderPolicy:Node.js 下的请求头安全策略
仅适用于 Node.js。 当你传入一个暴露 getHeaders() 的 Node.js FormData(如 form-data 包)时,axios 默认会把 getHeaders() 返回的所有请求头复制到请求上。这一行为保持了与 v1 的兼容,但当 FormData 对象来自不可信来源时存在风险——getHeaders() 可能覆盖 Authorization 等敏感头,甚至注入任意请求头。
设置 formDataHeaderPolicy: 'content-only' 后,只从 getHeaders() 中复制 Content-Type 和 Content-Length,其余请求头显式通过 headers 配置:
await axios.post('https://example.com/upload', form, {
formDataHeaderPolicy: 'content-only',
headers: {
Authorization: 'Bearer my-token',
},
});
默认值为 'legacy'(全量复制)。更多细节可参考请求配置参考中的 formDataHeaderPolicy 说明。
策略的实现集中在 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); // legacy:整体合并
return;
}
Object.entries(formHeaders || {}).forEach(([key, val]) => {
if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
headers.set(key, val); // content-only:仅白名单两个头
}
});
}
调用处在 resolveConfig.js,且注意 formDataHeaderPolicy 是通过 own('formDataHeaderPolicy')(自有属性)读取的,避免原型污染注入策略值。
支持的特殊键名后缀
axios 的 FormData 序列化器支持两种特殊键名后缀:
{}—— 该键对应的值用JSON.stringify序列化;[]—— 将扁平数组解包为多个同键字段。
注意:数组和 FileList 类型会默认展开(即使键名不带 [])。
从 toFormData.js 的 defaultVisitor 可以看到实现逻辑:
- 键以
{}结尾时:若metaTokens为true则保留完整键名(如obj2{}: '{"x":1}'),否则剥掉后缀;值统一走stringifyWithDepthLimit做受深度限制的 JSON 序列化; - 值是扁平数组(
isFlatArray),或值是 FileList / 键以[]结尾时:按indexes选项决定生成key[]、key还是key[i]三种键名形式。
配置 formSerializer:六个序列化选项
序列化器通过配置项 config.formSerializer 提供额外选项,用于处理特殊场景。选项解析在 toFormData.js 的 option() 中完成,缺省时逐项回落到默认值:
| 选项 | 类型/默认值 | 说明 |
|---|---|---|
visitor |
Function |
用户自定义访问函数,按自定义规则递归序列化数据对象到 FormData;返回值 true 表示继续递归进入该值 |
dots |
boolean = false |
使用点号而非方括号表示法序列化数组和对象(user.name vs user[name]) |
metaTokens |
boolean = true |
是否在键中保留特殊后缀(如 user{}: '{"name": "John"}')。后端解析器可据此自动将值解析为 JSON |
indexes |
null | false | true = false |
控制扁平数组解包时的键名形式(见下表) |
maxDepth |
number = 100 |
序列化器递归的最大嵌套深度;超限抛出 code 为 ERR_FORM_DATA_DEPTH_EXCEEDED 的 AxiosError。设为 Infinity 可禁用限制 |
Blob |
typeof Blob |
转换 ArrayBuffer 类值为规范兼容 FormData 时使用的 Blob 构造函数。仅在运行时以其他标识符提供兼容 Blob 构造器时覆盖 |
indexes 的三种取值:
null:不加方括号(arr: 1、arr: 2、arr: 3)false(默认):加空方括号(arr[]: 1、arr[]: 2、arr[]: 3)true:加带索引的方括号(arr[0]: 1、arr[1]: 2、arr[2]: 3)
对应源码即 toFormData.js 中 indexes === true ? renderKey([key], index, dots) : indexes === null ? key : key + '[]' 这一分支。
maxDepth 的保护通过 toFormData.js 的 throwIfMaxDepthExceeded 在递归 build 与 JSON 序列化两条路径上同时生效,默认值 100 由常量 DEFAULT_FORM_DATA_MAX_DEPTH 定义(与反向转换共用,保证 FormData 与 JSON 互转的对称性):
// Aumentar el límite para esquemas que legítimamente exceden 100 niveles:
axios.postForm('/api', data, { formSerializer: { maxDepth: 200 } });
安全提示:默认的 100 层限制是有意为之。服务端代码若把客户端可控的 JSON 直接作为
data转发给 axios,缺少该保护时会因深层嵌套触发调用栈溢出(DoS 攻击面)。只有当你的数据模式确实需要时才上调maxDepth。
此外从源码结构看,build 中还维护了一个 stack 数组用于循环引用检测(toFormData.js),一旦发现对象自引用会直接抛出 Circular reference detected 错误,避免无限递归。
完整示例:给定对象
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}]');
几个值得注意的细节:
arr是纯标量数组,走[]展开,所有元素共享arr[]键;arr2含嵌套数组(非扁平),因此按对象路径递归,键带真实索引(arr2[1][0]);users是对象数组,同样按路径展开为users[0][name]这类点/括号混合键;obj2{}后缀使其整个数组值被 JSON 化;- 键名会先做
trim()(见 toFormData.js 中key.trim()),空白键名会被规范化。
用 formToJSON 将 FormData 还原为 JSON
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,与正向序列化对称的几个要点:
- 路径解析正则
[^.[\]]+|\[([^.[\]]*)](formDataToJSON.js)只按.和[...]分组切分,保证user-name这类含连字符的键完整保留为字面量; - 同样受
DEFAULT_FORM_DATA_MAX_DEPTH(100)深度限制,超限抛出ERR_FORM_DATA_DEPTH_EXCEEDED,防止深层嵌套字段名导致服务端解析栈溢出; buildPath中对__proto__键显式短路跳过(formDataToJSON.js),防止通过构造字段名进行原型污染。
postForm / putForm / patchForm 快捷方法
axios 提供 postForm、putForm、patchForm 三个快捷方法,它们就是对应 HTTP 方法的封装,唯一区别是预置了 Content-Type: multipart/form-data 头。
源码在 Axios.js:post、put、patch、query 四个带数据的方法中,前三个额外生成 [method]Form 变体,其内部仅是向 mergeConfig 注入 headers: { 'Content-Type': 'multipart/form-data' }:
utils.forEach(['post', 'put', 'patch', 'query'], function forEachMethodWithData(method) {
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(
mergeConfig(config || {}, {
method,
headers: isForm ? { 'Content-Type': 'multipart/form-data' } : {},
url,
data,
})
);
};
}
Axios.prototype[method] = generateHTTPMethod();
// query 是只读语义,multipart 表单体不适合,因此不生成 queryForm
if (method !== 'query') {
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
}
});
因此 axios.postForm('/upload', data) 等价于 axios.post('/upload', data, { headers: { 'Content-Type': 'multipart/form-data' } }),而后者又会自动触发上文讲到的 toFormData 序列化路径——两个特性在此汇合。从源码结构看,没有 queryForm 的原因也写在了注释里:QUERY 是幂等只读方法,multipart 表单体不符合其语义。
验证参考
- 序列化行为的单测见 tests/unit/toFormData.test.js;
- 请求头策略相关的配置解析测试见 tests/unit/helpers/resolveConfig.test.js 与 tests/unit/adapters/http.test.js;
- 配置参考文档中
formDataHeaderPolicy的完整说明见 docs/es/pages/advanced/request-config.md。
小结:axios 的 multipart 能力分三层——直接传 FormData(浏览器交给平台生成 boundary,Node.js 走 getHeaders 合并策略)、对象自动序列化(toFormData + formSerializer 六选项 + 深度/循环引用双重防护)、以及 *Form 快捷方法(预置 Content-Type)。文件上传场景优先使用 postForm + 对象字面量,服务端转发不可信表单数据时务必理解 formDataHeaderPolicy 与 maxDepth 的安全语义。
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