news 2026/9/7 16:17:13

axios multipart/form-data 完全指南:手动构造、自动序列化与 formSerializer 配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
axios multipart/form-data 完全指南:手动构造、自动序列化与 formSerializer 配置

axios multipart/form-data 完全指南:手动构造、自动序列化与 formSerializer 配置

【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios

axios 对multipart/form-data格式的支持分三个层次:直接把FormData对象传入请求data(最基础的文件上传方式)、在Content-Type设为multipart/form-data时自动把普通 JS 对象序列化为FormData(v0.27.0 引入)、以及通过config.formSerializer精细控制数组展开、键名格式与嵌套深度等序列化行为。读完本文,你能掌握浏览器/Node.js 两种环境下构造 multipart 请求的正确姿势、formDataHeaderPolicy请求头安全策略,以及toFormDataformToJSON背后的序列化规则,并能在服务端安全处理客户端上传的表单数据。

手动创建 FormData 并发送请求

multipart/form-data格式发送请求的基本方式:创建一个FormData对象,向其追加数据,然后传入 axios 请求配置的data属性:

const formData = new FormData(); formData.append('foo', 'bar'); axios.post('https://httpbin.org/post', formData);

关键规则:对于浏览器、Web Worker 或 React Native 的原生FormData,不要手动设置Content-Type请求头——这些运行时会自行添加 multipart boundary。axios 在源码层面正是这样做的:在 resolveConfig 中,一旦检测到data是 FormData 且运行于标准浏览器、Web Worker 或 React Native 环境,会执行headers.setContentType(undefined),把 Content-Type 的决定权完全交还给底层传输层(XHR/fetch 会自动生成带 boundary 的正确头)。

在 Node.js 中,可以使用form-data库:

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 侧与浏览器侧的处理差异:当 Node.js 的FormData对象暴露getHeaders()方法(form-data包即如此)时,axios 会调用它并合并返回的请求头(见下文「Node.js FormData 的请求头策略」一节),boundary 由form-data库自身提供。

自动序列化为 FormData

从 v0.27.0 起,如果请求的Content-Type请求头设置为multipart/form-data,axios 支持自动将对象序列化为FormData对象——可以直接把 JavaScript 对象传入data,无需手动append

import axios from 'axios'; axios .post( 'https://httpbin.org/post', { x: 1 }, { headers: { 'Content-Type': 'multipart/form-data', }, } ) .then(({ data }) => console.log(data));

这条自动序列化链路的实现入口在默认transformRequest中(lib/defaults/index.js):当 payload 是普通对象、且 Content-Type 包含multipart/form-data(或 data 是 FileList)时,axios 读取实例上的env.FormDataformSerializer配置,调用toFormData(data, _FormData && new _FormData(), formSerializer)完成转换:

// lib/defaults/index.js(简化自源码) 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 ); }

两个值得注意的细节:

  • 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));
  • 直接传 FileList 会被包装为files[]。上面的源码可见,isFileList为真时 data 被包成{ 'files[]': data }再序列化,即所有文件以同名键files[]展开追加。

Node.js FormData 的请求头策略(formDataHeaderPolicy)

当你传入一个暴露getHeaders()的 Node.jsFormData对象(例如form-data包)时,axios 默认会将它返回的所有请求头复制到请求上。这保留了 v1 的兼容性,但如果FormData对象来自不可信来源,可能会出问题——getHeaders()可能覆盖Authorization等请求头或注入任意请求头。

对应实现是 setFormDataHeaders:

// lib/core/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); } }); }

策略判断只在policy === 'content-only'时收窄为仅复制Content-TypeContent-Length,其余情况全部合并(legacy 行为)。该函数在 resolveConfig 与 http 适配器 http.js 中被调用,并读取请求自身的formDataHeaderPolicy配置(通过own()只读自身属性,避免原型污染干扰)。

设置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'。该配置项的完整说明见请求配置参考 request-config 中的formDataHeaderPolicy章节,Node.js 适配器侧的行为另有 http.test.js 与 resolveConfig.test.js 两类测试覆盖。

支持的特殊结尾

axios 的 FormData 序列化器支持两种特殊结尾,用于在键名中声明该值应如何序列化:

  • {}— 使用JSON.stringify序列化该值
  • []— 将类数组对象展开为具有相同键的独立字段

注意:展开/扩展操作默认应用于数组和 FileList 对象,即数组不需要[]结尾也会被展开。

这两个行为的源码位置在 toFormData.js 的 defaultVisitor:

function defaultVisitor(value, key, path) { let arr = value; if (value && !path && typeof value === 'object') { if (utils.endsWith(key, '{}')) { // 顶层键以 {} 结尾:整体 JSON.stringify key = metaTokens ? key : key.slice(0, -2); value = stringifyWithDepthLimit(value, 1); } else if ( (utils.isArray(value) && isFlatArray(value)) || ((utils.isFileList(value) || utils.endsWith(key, '[]')) && (arr = utils.toArray(value))) ) { // 扁平数组 / FileList / 键以 [] 结尾:逐项展开为同键多字段 key = removeBrackets(key); arr.forEach(function each(el, index) { !(utils.isUndefined(el) || el === null) && formData.append( indexes === true ? renderKey([key], index, dots) : indexes === null ? key : key + '[]', convertValue(el) ); }); return false; } } // ... }

可以推断:{}结尾仅在顶层键path为空)上生效;嵌套层级中的对象一律递归展开为父键[子键]形式。indexes选项则在展开扁平数组时决定键名形态,见下一节。

配置 FormData 序列化器

FormData 序列化器通过config.formSerializer对象属性支持以下选项,用于处理特殊情况:

选项默认值说明
visitor: Function内置defaultVisitor用户自定义的访问者函数,递归调用以按自定义规则将数据对象序列化为 FormData
dots: booleanfalse使用点号表示法代替方括号来序列化数组和对象
metaTokens: booleantrue在 FormData 键中保留{}等特殊结尾(如user{}: '{"name": "John"}')。后端 body 解析器可利用此元信息自动将值解析为 JSON
indexes: null \| false \| truefalse控制如何为扁平类数组的展开键添加索引(详见下表)
maxDepth: number100序列化器递归的最大对象嵌套深度,超出抛出code: 'ERR_FORM_DATA_DEPTH_EXCEEDED'AxiosError;设为Infinity禁用限制
Blob: typeof Blob运行时Blob在符合规范的FormData中转换类 ArrayBuffer 值时使用的 Blob 构造函数,仅当运行时以其他标识符提供兼容的Blob时才需覆盖

indexes的三种取值对应三种展开形态:

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

这些选项在 toFormData 中逐项读取并设置默认值:

const metaTokens = option('metaTokens', true); const visitor = option('visitor') || defaultVisitor; const dots = option('dots', false); const indexes = option('indexes', false); const _Blob = option('Blob') || (typeof Blob !== 'undefined' && Blob); const maxDepth = option('maxDepth', DEFAULT_FORM_DATA_MAX_DEPTH);

其中DEFAULT_FORM_DATA_MAX_DEPTH = 100定义在 toFormData.js,并与反向转换formDataToJSON共用同一常量,保证 FormData 与 JSON 往返转换的对称性。

maxDepth 深度保护

maxDepth限制是有意为之的安全机制。默认限制 100 保护服务端应用免受深层嵌套载荷的 DoS 攻击:源码中build()递归入口每层都会throwIfMaxDepthExceeded(depth),而{}结尾的 JSON 序列化路径也通过stringifyWithDepthLimit在 replacer 中逐层检查深度。当 schema 确实需要超过 100 层嵌套时,可提高限制:

// 当 schema 确实需要超过 100 层嵌套时,可提高限制: axios.postForm('/api', data, { formSerializer: { maxDepth: 200 } });

安全提示:将客户端控制的 JSON 作为data转发给 axios 的服务端代码,如果没有此保护,容易发生调用栈溢出。除非你的 schema 确实需要,否则不要提高maxDepth

对应的测试用例见 toFormData.test.js:maxDepth: 200时 150 层嵌套可正常序列化、maxDepth: 5时 10 层嵌套被拒绝、maxDepth: Infinity时 500 层嵌套也不触发深度保护。该保护还延伸到AxiosURLSearchParams的 query 参数序列化路径(见 AxiosURLSearchParams.js 同样复用toFormData)。

另外两点实现层面的行为值得了解(从源码结构看):

  • 循环引用检测build()stack数组记录当前递归路径,若发现某个值已在栈中则抛出Circular reference detected in <path>错误,防止自引用对象导致死递归。
  • 值转换规则convertValue()null转空字符串、Date 转 ISO 字符串、布尔转字符串;ArrayBuffer/TypedArray 在规范合规的 FormData 中转为 Blob,否则在 Node.js 中转为 Buffer,两者都不可用时抛出Blob is not supported. Use a Buffer instead.

序列化过程示例

对于以下对象:

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[0]arr2[1][0]arr2[2]users数组的元素是对象,递归展开为users[0][name]形式;顶层键obj2{}保留{}结尾(metaTokens默认true),其值整体JSON.stringify

将 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 用正则/[^.[\]]+|\[([^.[\]]*)\]/gfoo[x][y]foo.x.y等字段名解析为路径段数组,再递归buildPath构建嵌套结构——注释中明确说明user-nameuser name这类键会被整体保留为字面键,不会被拆分。同名键重复出现时会把已有值与新值合并为数组,实现多值字段(如arr[]展开后的多个同键字段)到数组的还原。该函数同样受DEFAULT_FORM_DATA_MAX_DEPTH = 100的深度保护,并显式跳过__proto__键以防原型污染。axios.formToJSON的导出见 axios.js:

axios.formToJSON = (thing) => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);

注意它还接受原生 HTML<form>元素,内部先转换为FormData再解析。

postForm / putForm / patchForm 快捷方法

axios 支持以下快捷方法:postFormputFormpatchForm,它们分别是相应 HTTP 方法的变体,并预设Content-Type请求头为multipart/form-data——等价于手动在 config 中指定该 Content-Type 并触发自动序列化,但更简洁。

它们在 Axios.js 中与post/put/patch在同一循环中生成:

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 is a safe/idempotent read method; multipart form bodies don't fit // its semantics, so no queryForm shorthand is generated. if (method !== 'query') { Axios.prototype[method + 'Form'] = generateHTTPMethod(true); } });

从源码结构看有一个值得注意的细节:query方法虽然也在此循环中,但不会生成queryForm——源码注释说明 QUERY 是安全/幂等的读方法,multipart 表单体的语义与之不符。

小结与延伸阅读

本文覆盖了 axios multipart/form-data 支持的完整链路:手动FormData直传(浏览器不手动设 Content-Type,Node.js 用form-data库)、Content-Type: multipart/form-data触发的自动对象序列化、formDataHeaderPolicy请求头策略、{}/[]特殊结尾、formSerializer的六个配置项(尤其maxDepth安全限制)、formToJSON反向转换,以及postForm/putForm/patchForm快捷方法。

  • 默认请求转换与 FormData 判定逻辑:defaults/index.js
  • 序列化器核心实现与深度保护:helpers/toFormData.js
  • 反向转换实现:helpers/formDataToJSON.js
  • 请求头策略合并:core/setFormDataHeaders.js、helpers/resolveConfig.js
  • 序列化器测试(含 maxDepth 用例):tests/unit/toFormData.test.js
  • 表单上传实战示例:examples/postMultipartFormData、大文件上传示例 examples/upload
  • formDataHeaderPolicy完整配置说明:docs/pages/advanced/request-config.md
  • 相关的 URL 编码表单格式(复用同一序列化器):x-www-form-urlencoded-format

【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 16:17:02

空气源热泵热水器:从原理、能效到选型安装全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:16:09

WorkBuddy双模型限免实测:Hy4 preview与Hy3选型与配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 16:15:34

Python构建电影推荐系统:KNN协同过滤与API接口实战

作为一个靠Python吃饭的人&#xff0c;我太清楚“算法”和“API”这两个词对新手意味着什么了。很多人学完基础语法、爬虫、数据分析那一套之后&#xff0c;会觉得“我啥都会了&#xff0c;但啥也做不出来”。而电影推荐系统这个项目&#xff0c;恰好就是打通“理论”到“实战”…

作者头像 李华
网站建设 2026/9/7 16:14:45

Deepseek相关技术应用与行业发展趋势解析

谁懂啊&#xff0c;2026届硕博新生们&#xff01; 刚入学、刚转博&#xff0c;最崩溃的瞬间&#xff0c;一定是面对开题报告的那一刻&#xff1a; 方向没定&#xff0c;文献没读&#xff0c;框架搭不出来&#xff0c;导师一问三不知&#xff1b;好不容易憋出一版&#xff0c;…

作者头像 李华
网站建设 2026/9/7 16:13:57

容错模型预测控制(FT-MPC)原理与Matlab仿真:从故障诊断到控制重构

1. 从“能控”到“能扛”&#xff1a;为什么线性时不变系统需要容错MPC 搞控制的人都有过这种体会&#xff1a;模型预测控制&#xff08;MPC&#xff09;在仿真里跑得行云流水&#xff0c;跟踪精度、约束满足样样漂亮&#xff0c;但一放到真实设备上&#xff0c;就总有种“纸上…

作者头像 李华
网站建设 2026/9/7 16:13:15

VMP与JSVMP通用分析:从虚拟机原理到动态插桩还原

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华