首页
/ Axios multipart/form-data 完整指南:FormData 自动序列化、formSerializer 配置与 formToJSON 反向转换

Axios multipart/form-data 完整指南:FormData 自动序列化、formSerializer 配置与 formToJSON 反向转换

2026-09-04 22:29:50作者:段琳惟

本文围绕 Axios 的 multipart/form-data 发送能力展开:从浏览器与 Node.js 环境下的基础提交,到 v0.27.0 引入的对象自动序列化机制、formSerializer 各配置项(dotsmetaTokensindexesmaxDepth 等)的取值与行为,再到 Node.js 下 formDataHeaderPolicy 头安全策略与 formToJSON() 的反向解析。读完你可以完整掌握 Axios 表单序列化的规则细节,并结合 toFormData.jssetFormDataHeaders.jsformDataToJSON.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-TypeContent-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: 1arr: 2arr: 3);false(默认)— 加空括号(arr[]: 1…);true — 加真实下标(arr[0]: 1arr[1]: 2arr[2]: 3
maxDepth number = 100 序列化递归的最大对象嵌套深度。超限抛出 code 为 ERR_FORM_DATA_DEPTH_EXCEEDEDAxiosError,用于防御服务端通过深层嵌套负载发起的 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_EXCEEDED code 的 AxiosErrortoFormData.js);对 {} 终结符触发的 JSON.stringify 也内置了等价的深度限制 stringifyWithDepthLimit()
  • 循环引用检测build() 维护一个 stack,遇到重复对象立即抛出 Circular reference detected 错误,避免死递归(toFormData.js)。
  • indexes 三分支:平铺数组展开时的字段名生成就写死在 defaultVisitor 中——indexes === truerenderKey([key], index, dots) 生成 arr[0]indexes === null 直接用 key,其余情况(默认 false)拼 key + '[]'toFormData.js)。
  • metaTokens{} 终结符在 metaTokenstrue 时原样保留在字段名里,否则被 key.slice(0, -2) 剥掉(toFormData.js)。
  • dots:字段路径渲染由 renderKey(path, key, dots) 完成,dots 为真时用 . 连接,否则用方括号包裹(toFormData.js)。
  • 值类型转换Date 转为 ISO 字符串、布尔转字符串、ArrayBuffer/TypedArray 在有规范兼容 Blob 时转 Blob、在 Node 有 Buffer 时转 Buffer(convertValuetoFormData.js)。
  • visitor 扩展点:自定义 visitor 以 FormData 为 this 被调用,返回 true 表示继续递归遍历该值;同时可访问 defaultVisitorconvertValueisVisitable 及一组 is* 类型判断辅助函数(exposedHelperstoFormData.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)。
  • 数字键(01…)在目标位置自动形成数组,非数字键还原为对象属性;解析同样受 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 还提供三个快捷方法:postFormputFormpatchForm。它们与对应的 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-Typemultipart/form-dataformSerializervisitor / dots / metaTokens / indexes / maxDepth / Blob 覆盖几乎全部特殊场景,maxDepth 默认 100 层是服务端 DoS 防护线。
  • 终结符 {}(JSON 化)与 [](字段展开)是字段级的序列化开关;formToJSON().[] 三个结构性分隔符做无损逆向解析,并与序列化器共享同一深度上限。
登录后查看全文
热门项目推荐
相关项目推荐