axios 表单编码实战:application/x-www-form-urlencoded 的 URLSearchParams 自动序列化与深度限制
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
axios 默认的transformRequest会把 JavaScript 对象序列化为 JSON,而对接传统表单接口、老旧后端或遵循 HTML 表单规范的 API 时,你需要发送application/x-www-form-urlencoded编码的数据。本文围绕 axios 官方文档 x-www-form-urlencoded-format 展开,讲清三种序列化方案(URLSearchParams、qs、Node 原生querystring)的适用场景,并结合源码剖析 axios 自 v0.21.0 起的「自动序列化」机制、嵌套键的命名规则,以及maxDepth深度限制背后的安全设计,读完你可以直接在项目中编写可复现的表单提交代码,并理解请求体在 axios 内部的完整处理链路。
一、为什么默认行为不是表单编码
axios 对请求体的默认序列化逻辑在 lib/defaults/index.js 的transformRequest中。关键分支如下:
- 若数据是
URLSearchParams实例(utils.isURLSearchParams(data)为真),则把Content-Type设为application/x-www-form-urlencoded;charset=utf-8,并调用data.toString()得到编码字符串(见 lib/defaults/index.js#L72-L75); - 若数据是普通对象,且
Content-Type中已包含application/x-www-form-urlencoded,则走toURLEncodedForm(data, formSerializer).toString()进行自动序列化(见 lib/defaults/index.js#L79-L83); - 其他普通对象最终落入
stringifySafely,即以JSON.stringify序列化并设置application/json。
因此「发表单而不是 JSON」有两条正路:手动构造URLSearchParams交给 axios 透传,或者显式指定Content-Type让 axios 替你序列化。下面分别介绍。
二、方案一:直接使用 URLSearchParams(现代环境首选)
axios 默认把对象序列化为 JSON;要发送application/x-www-form-urlencoded数据,最标准的做法是使用 Web 平台普遍支持的URLSearchParams接口(Node.js 自 v10 起内置,见node:url模块文档):
const params = new URLSearchParams({ foo: 'bar' }); params.append('extraparam', 'value'); axios.post('/foo', params);这条路径的底层依据就在上文提到的默认transformRequest:axios 检测到URLSearchParams实例后,不会再尝试 JSON 序列化,而是直接把实例转成'foo=bar&extraparam=value'这样的字符串,并补上带 UTF-8 字符集的Content-Type头。你可以通过append自由控制键的顺序与重复键,这也是「手动控制编码结果」时最直接的把手。
三、方案二:qs 库序列化(面向老旧环境)
对于更老的浏览器或没有URLSearchParams的环境,可以使用 [qs] 类库将对象序列化为application/x-www-form-urlencoded字符串(此处不贴外部链接,npm 包名为qs):
const qs = require('qs'); axios.post('/foo', qs.stringify({ bar: 123 }));当你需要对请求头和 HTTP 方法做完全控制时,把qs.stringify的产物作为data传入,并显式声明Content-Type:
import qs from 'qs'; const data = { bar: 123 }; const options = { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify(data), url: '/foo', }; axios(options);这里字符串data不会被 JSON 二次序列化(transformRequest只处理对象载荷),所以显式设置Content-Type是保证服务端正确解析的关键。
附:Node 原生 querystring(已弃用)
在非常老的 Node.js 版本中,还可以使用 Node 自带的querystring模块:
const querystring = require('querystring'); axios.post('https://something.com/', querystring.stringify({ foo: 'bar' }));注意:该模块自 Node.js v16 起已被弃用,新代码请优先选择URLSearchParams或qs。另外,如果你需要序列化嵌套对象,官方文档明确建议优先使用qs,因为原生querystring对嵌套对象这一用例存在已知问题(会生成类似a[0]=1但不做 URL 编码的畸形输出)。
四、自动序列化:v0.21.0 起对象自动转为 URLSearchParams
从 axios v0.21.0 开始,只要请求配置的Content-Type设为application/x-www-form-urlencoded,axios 就会把data中的 JavaScript 对象自动序列化为URLSearchParams。以postForm为例(postForm默认将Content-Type置为multipart/form-data,此处通过 headers 覆盖为 urlencoded,从而命中自动序列化分支):
const data = { x: 1, arr: [1, 2, 3], arr2: [1, [2], 3], users: [ { name: 'Peter', surname: 'Griffin' }, { name: 'Thomas', surname: 'Anderson' }, ], }; await axios.postForm('https://postman-echo.com/post', data, { headers: { 'content-type': 'application/x-www-form-urlencoded' }, });data对象会被自动序列化为application/x-www-form-urlencoded格式发送,服务端收到的字段为:
{ "x": "1", "arr[]": ["1", "2", "3"], "arr2[0]": "1", "arr2[1][0]": "2", "arr2[2]": "3", "users[0][name]": "Peter", "users[0][surname]": "Griffin", "users[1][name]": "Thomas", "users[1][surname]": "Anderson" }源码链路:从 data 到请求体字符串
这条自动序列化的完整链路可以逐层对照源码:
- 入口:
postForm等*Form方法在 lib/core/Axios.js#L281-L306 中由generateHTTPMethod(true)生成,默认头为multipart/form-data;若像上面的例子覆盖成application/x-www-form-urlencoded,则请求头优先,命中下面的分支。 - 分支判断:默认
transformRequest中contentType.indexOf('application/x-www-form-urlencoded') > -1时调用toURLEncodedForm(data, formSerializer).toString()(lib/defaults/index.js#L79-L83)。注意formSerializer来自请求配置(own(this, 'formSerializer')),它是透传给底层序列化器的选项包。 - 适配器:lib/helpers/toURLEncodedForm.js 本身只有 19 行——它复用通用的
toFormData遍历器,把「目标容器」换成平台提供的URLSearchParams类实例,并注入一个自定义visitor:在 Node 环境下遇到Buffer值时,将其以 base64 字符串追加而非作为文件内容处理,其余情况委托给默认访问器。 - 嵌套键的生成规则:真正决定上面 JSON 中
arr[]、users[0][name]这类键名的是 lib/helpers/toFormData.js 中defaultVisitor与renderKey的组合逻辑:- 扁平数组(元素均不可再遍历)→ 键追加
[]后缀,因此arr: [1,2,3]输出arr[]; - 可遍历的数组/对象→ 递归下降,用
path.concat(key)渲染为arr2[1][0]、users[0][name]这类方括号路径; - 若选项
indexes为true,扁平数组会改用下标键(arr[0]);若为null,则完全不加分隔符(多值同名键)。
- 扁平数组(元素均不可再遍历)→ 键追加
- 编码与拼接:
toString()阶段的百分号编码在 lib/helpers/AxiosURLSearchParams.js#L13-L25 中实现:先encodeURIComponent,再把!'()~等「非保留字符」还原为原字符,并把%20换成+——这正是 HTML 表单编码与标准 URI 编码的差异所在,保证与application/x-www-form-urlencoded规范对齐。
如果你的后端(如 express 的body-parser)以extended: true解析表单体,服务端即可自动还原出与客户端相同的嵌套对象结构。
五、params 序列化的深度限制(maxDepth)
当 axios 通过AxiosURLSearchParams序列化params对象时(用于 URL 查询串),底层复用的正是上面同一个toFormData递归遍历器。为此 axios 引入了maxDepth选项(默认 100),对应源码常量DEFAULT_FORM_DATA_MAX_DEPTH = 100(lib/helpers/toFormData.js#L9-L11)。当嵌套层级超限,axios 抛出携带code: 'ERR_FORM_DATA_DEPTH_EXCEEDED'的AxiosError,而不是任由递归触发栈溢出:
// 如果你的 params 对象确实需要超过 100 层嵌套: axios.get('/api', { params: deepObject, paramsSerializer: { maxDepth: 200 } });安全提示:只有在业务模型确实需要时才调高
maxDepth。默认值 100 的作用,是保护那些「把客户端可控数据原样转发为 params」的服务端代码,使其免受深度嵌套对象带来的 DoS 攻击。
几个实现细节值得注意:
- 超限抛错的时机:
throwIfMaxDepthExceeded在每次递归进入build时检查(lib/helpers/toFormData.js#L152-L159),因此错误发生在请求分发阶段、适配器被调用之前。单测 tests/unit/core/dispatchRequest.test.js 断言了抛出的必须是AxiosError且adapterCalled === false。 - 选项的防污染读取:
toFormData通过utils.getSafeProp(options, name)读取maxDepth等选项(lib/helpers/toFormData.js#L102-L113),只有Object.prototype上被注入的maxDepth/visitor会被忽略,tests/unit/toFormData.test.js 专门验证了这一行为。 - params 链路上的透传:
paramsSerializer若为对象,会被校验只包含encode/serialize之外的宽松选项(见 lib/core/Axios.js#L111-L126),随后在 lib/helpers/buildURL.js#L49-L57 中整体作为options传入new AxiosURLSearchParams(params, _options),再进入toFormData(params, this, options),所以maxDepth沿这条链路生效。 Blob选项的作用边界:共享的类型成员SerializerOptions.Blob只影响「面向规范FormData的序列化」(控制二进制是否包装为Blob上传),对序列化到URLSearchParams的过程没有任何效果——在 Node 中二进制走的是上文提到的 base64visitor分支。
六、服务端回显示例
把客户端产物放到一个可运行的服务端进行回显,是最直观的验证方式。官方文档给出的 express 示例:
var app = express(); app.use(bodyParser.urlencoded({ extended: true })); // 支持编码的表单体(可还原嵌套对象) app.post('/', function (req, res, next) { // 以 JSON 回显请求体 res.send(JSON.stringify(req.body)); }); server = app.listen(3000);要点是extended: true:只有开启后,body-parser才能把users[0][name]=Peter这类方括号键解析回嵌套对象,实现与客户端发送对象的结构对齐;若不开启,服务端只会得到扁平的字符串键值对(如"users[0][name]": "Peter")。
七、方案选型小结
| 场景 | 推荐做法 | 依据 |
|---|---|---|
| 现代浏览器 / Node ≥ 10,简单平铺数据 | 直接传URLSearchParams实例 | lib/defaults/index.js#L72-L75 |
| 需要嵌套对象自动编码 | data传对象 +content-type: application/x-www-form-urlencoded(v0.21.0+) | lib/helpers/toURLEncodedForm.js |
| 老环境 / 对编码细节(嵌套、下标)有强控制需求 | 自行qs.stringify后传字符串 + 显式头 | lib/defaults/index.js#L43-L106 |
| 仅 Node 老版本、一次性脚本 | 原生querystring(已弃用,不推荐新代码) | Node.js v16 起弃用 |
URL 查询串params可能深度嵌套 | paramsSerializer: { maxDepth }显式声明上限 | lib/helpers/buildURL.js#L49-L57 |
理解上面的源码链路后,你在遇到「对象发出去变成了 JSON」「服务端收不到嵌套字段」这类问题时,就可以直接定位到transformRequest的哪个分支生效、Content-Type是否命中 urlencoded 判断,而无需靠试错。
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考