首页
/ axios 中 multipart/form-data 上传实战:手动 FormData、自动序列化与 formSerializer 深度解析

axios 中 multipart/form-data 上传实战:手动 FormData、自动序列化与 formSerializer 深度解析

2026-09-05 10:10:23作者:廉彬冶Miranda

axios 支持以 multipart/form-data 格式发送请求,这是文件上传场景最常用的格式。本文覆盖从"手动构造 FormData"到"对象自动序列化"的完整链路:浏览器与 Node.js 环境下 FormData 的构造差异、formDataHeaderPolicy 请求头安全策略、{}[] 特殊键名后缀的行为规则、config.formSerializer 的六项序列化选项,以及 formToJSON 的反向转换能力,并结合 lib/helpers/toFormData.jslib/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.jsconst _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-TypeContent-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.jsdefaultVisitor 可以看到实现逻辑:

  • 键以 {} 结尾时:若 metaTokenstrue 则保留完整键名(如 obj2{}: '{"x":1}'),否则剥掉后缀;值统一走 stringifyWithDepthLimit 做受深度限制的 JSON 序列化;
  • 值是扁平数组(isFlatArray),或值是 FileList / 键以 [] 结尾时:按 indexes 选项决定生成 key[]key 还是 key[i] 三种键名形式。

配置 formSerializer:六个序列化选项

序列化器通过配置项 config.formSerializer 提供额外选项,用于处理特殊场景。选项解析在 toFormData.jsoption() 中完成,缺省时逐项回落到默认值:

选项 类型/默认值 说明
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_EXCEEDEDAxiosError。设为 Infinity 可禁用限制
Blob typeof Blob 转换 ArrayBuffer 类值为规范兼容 FormData 时使用的 Blob 构造函数。仅在运行时以其他标识符提供兼容 Blob 构造器时覆盖

indexes 的三种取值:

  • null:不加方括号(arr: 1arr: 2arr: 3
  • false(默认):加空方括号(arr[]: 1arr[]: 2arr[]: 3
  • true:加带索引的方括号(arr[0]: 1arr[1]: 2arr[2]: 3

对应源码即 toFormData.jsindexes === true ? renderKey([key], index, dots) : indexes === null ? key : key + '[]' 这一分支。

maxDepth 的保护通过 toFormData.jsthrowIfMaxDepthExceeded 在递归 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.jskey.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 提供 postFormputFormpatchForm 三个快捷方法,它们就是对应 HTTP 方法的封装,唯一区别是预置了 Content-Type: multipart/form-data 头。

源码在 Axios.jspostputpatchquery 四个带数据的方法中,前三个额外生成 [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 表单体不符合其语义。

验证参考

小结:axios 的 multipart 能力分三层——直接传 FormData(浏览器交给平台生成 boundary,Node.js 走 getHeaders 合并策略)、对象自动序列化(toFormData + formSerializer 六选项 + 深度/循环引用双重防护)、以及 *Form 快捷方法(预置 Content-Type)。文件上传场景优先使用 postForm + 对象字面量,服务端转发不可信表单数据时务必理解 formDataHeaderPolicymaxDepth 的安全语义。

登录后查看全文
热门项目推荐
相关项目推荐