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请求头安全策略,以及toFormData、formToJSON背后的序列化规则,并能在服务端安全处理客户端上传的表单数据。
手动创建 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.FormData和formSerializer配置,调用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-Type与Content-Length,其余情况全部合并(legacy 行为)。该函数在 resolveConfig 与 http 适配器 http.js 中被调用,并读取请求自身的formDataHeaderPolicy配置(通过own()只读自身属性,避免原型污染干扰)。
设置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'。该配置项的完整说明见请求配置参考 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: boolean | false | 使用点号表示法代替方括号来序列化数组和对象 |
metaTokens: boolean | true | 在 FormData 键中保留{}等特殊结尾(如user{}: '{"name": "John"}')。后端 body 解析器可利用此元信息自动将值解析为 JSON |
indexes: null \| false \| true | false | 控制如何为扁平类数组的展开键添加索引(详见下表) |
maxDepth: number | 100 | 序列化器递归的最大对象嵌套深度,超出抛出code: 'ERR_FORM_DATA_DEPTH_EXCEEDED'的AxiosError;设为Infinity禁用限制 |
Blob: typeof Blob | 运行时Blob | 在符合规范的FormData中转换类 ArrayBuffer 值时使用的 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 中逐项读取并设置默认值:
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 用正则/[^.[\]]+|\[([^.[\]]*)\]/g把foo[x][y]、foo.x.y等字段名解析为路径段数组,再递归buildPath构建嵌套结构——注释中明确说明user-name、user 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 支持以下快捷方法:postForm、putForm、patchForm,它们分别是相应 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),仅供参考