axios 表单请求实战:x-www-form-urlencoded 的 URLSearchParams、自动序列化与深度限制
axios 默认将 JavaScript 对象序列化为 JSON 发送。当后端要求以 application/x-www-form-urlencoded(传统 HTML 表单)格式提交数据时,需要理解本文覆盖的三条路径:直接使用 URLSearchParams、使用 qs/querystring 手动序列化、以及 axios v0.21.0 引入的「对象自动序列化」机制。读完本文,你将掌握表单编码请求的完整写法、嵌套对象的键名规则、底层序列化器的源码实现,以及 maxDepth 深度保护参数的原理与安全意义。
一、默认行为:对象被序列化为 JSON
axios 的请求体转换发生在默认 transformRequest 中。从 lib/defaults/index.js 可以看到默认逻辑:
transformRequest: [
function transformRequest(data, headers) {
const contentType = headers.getContentType() || '';
const hasJSONContentType = contentType.indexOf('application/json') > -1;
const isObjectPayload = utils.isObject(data);
// ...
if (isObjectPayload || hasJSONContentType) {
headers.setContentType('application/json', false);
return stringifySafely(data); // JSON.stringify
}
return data;
},
],
也就是说:只要你传入的是一个普通对象且没有声明表单类型的 Content-Type,axios 就会 JSON.stringify 它并把请求头设为 application/json。要走表单编码,必须显式声明 Content-Type: application/x-www-form-urlencoded,或直接把数据写成 URLSearchParams 实例。
二、方案一:URLSearchParams(推荐)
URLSearchParams 是标准 Web API,在绝大多数浏览器中得到支持,Node.js 从 v10(2018 年)开始内置。它是发送表单编码数据的首选方式:
const params = new URLSearchParams({ foo: 'bar' });
params.append('extraparam', 'value');
axios.post('/foo', params);
axios 对 URLSearchParams 有专门的处理分支。在 lib/defaults/index.js 中:
if (utils.isURLSearchParams(data)) {
headers.setContentType('application/x-www-form-urlencoded;charset=utf-8', false);
return data.toString();
}
即:当 data 是 URLSearchParams 实例时,axios 会自动把请求头设置为 application/x-www-form-urlencoded;charset=utf-8,并调用其 toString() 得到 foo=bar&extraparam=value 形式的请求体。你甚至不需要手动设置 Content-Type。
三、方案二:qs / querystring 手动序列化(兼容老环境)
针对较旧的浏览器或没有 URLSearchParams 的环境,可以用 qs 库把对象序列化为表单编码字符串:
const qs = require('qs');
axios.post('/foo', qs.stringify({ bar: 123 }));
此时 data 是一个字符串,transformRequest 会跳过所有对象分支直接透传。但字符串不会自动携带表单 Content-Type,如果需要完全控制请求头和请求方法,应显式设置 Content-Type,并像普通配置一样调用 axios:
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);
Node.js 内置 querystring 模块(不推荐用于新代码)
在非常老的 Node.js 版本中,还可以使用内置 querystring 模块:
const querystring = require('querystring');
axios.post('https://something.com/', querystring.stringify({ foo: 'bar' }));
需要注意两点:
- 该模块自 Node.js v16 起已被标记为废弃(deprecated),新代码应优先选择
URLSearchParams或qs; querystring对嵌套对象存在已知问题,需要序列化嵌套对象时请优先使用qs。
四、v0.21.0 起:对象自动序列化为 URLSearchParams
从 v0.21.0 开始,axios 具备了一个非常实用的能力:当 Content-Type 被设置为 application/x-www-form-urlencoded 时,直接传 JavaScript 对象,axios 会自动将其序列化为表单编码格式。例如向 POST 请求传数据:
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' },
});
这里有一个值得注意的细节:axios.postForm 由 lib/core/Axios.js 中的 generateHTTPMethod(true) 生成,默认会注入 'Content-Type': 'multipart/form-data' 头:
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
// 其中 isForm 为 true 时:
// headers: { 'Content-Type': 'multipart/form-data' }
但请求配置中的 headers 会在 mergeConfig 阶段覆盖实例默认头,因此上面示例中显式传入的 application/x-www-form-urlencoded 会生效,最终进入 transformRequest 的这条分支(lib/defaults/index.js):
if (isObjectPayload) {
const formSerializer = own(this, 'formSerializer');
if (contentType.indexOf('application/x-www-form-urlencoded') > -1) {
return toURLEncodedForm(data, formSerializer).toString();
}
// ...multipart 分支
}
服务器实际收到的键名
上述 data 对象会被自动序列化为 URLSearchParams 并发送,服务器(如 postman-echo)收到的数据为:
{
"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"
}
键名规则与序列化器的遍历逻辑一一对应(见下文第五节的 defaultVisitor):
- 扁平数组(元素全部非可遍历)→ 重复的
arr[]=1&arr[]=2&...形式; - 混合数组/嵌套结构 → 带下标的
arr2[0]、arr2[1][0]形式; - 对象数组 →
users[0][name]这类深层方括号键名。
如果你的后端 body 解析器支持嵌套对象解码(如 express 的 body-parser 配合 extended: true),服务端可以自动还原出与原始对象相同的结构:
var app = express();
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
app.post('/', function (req, res, next) {
// echo body as JSON
res.send(JSON.stringify(req.body));
});
server = app.listen(3000);
五、源码深入:toURLEncodedForm 与共享递归遍历器
自动序列化的入口是 lib/helpers/toURLEncodedForm.js,实现非常薄——它复用了 FormData 序列化器的同一个递归遍历器 toFormData,只是目标容器换成了 URLSearchParams:
export default function toURLEncodedForm(data, options) {
return toFormData(data, new platform.classes.URLSearchParams(), {
visitor: function (value, key, path, helpers) {
if (platform.isNode && utils.isBuffer(value)) {
this.append(key, value.toString('base64'));
return false;
}
return helpers.defaultVisitor.apply(this, arguments);
},
...options,
});
}
两个值得注意的实现事实:
- 容器按平台选择:
platform.classes.URLSearchParams分别来自 lib/platform/browser/classes/ 与 lib/platform/node/classes/,保证浏览器与 Node 环境行为一致; - Node 下 Buffer 特判:
Buffer值会被toString('base64')后追加,避免被当作普通对象展开。
核心的递归遍历器位于 lib/helpers/toFormData.js,其 defaultVisitor 决定了上文看到的键名规则:
function defaultVisitor(value, key, path) {
// 扁平数组或 key 以 '[]' 结尾 → 去掉括号后对每个元素 append(key + '[]')
// 普通对象/数组 → 返回 true,交给外层 build() 以 path.concat(key) 递归下钻
// 叶子值 → formData.append(renderKey(path, key, dots), convertValue(value))
}
其中 renderKey(lib/helpers/toFormData.js)负责把路径数组 ['arr2', '1', '0'] 渲染为 arr2[1][0](默认方括号风格;开启 dots 选项时则为点号风格)。对 null、Date、Boolean 等值的归一化由 convertValue 完成(null → 空串,Date → ISO 字符串)。
另外,共享的 Blob 序列化选项(SerializerOptions.Blob)只作用于符合规范的 FormData:在 lib/helpers/toFormData.js 中,useBlob = _Blob && utils.isSpecCompliantForm(formData),URLSearchParams 容器不满足该条件,因此该选项对表单编码序列化没有效果。
六、params 序列化:GET 查询串的 maxDepth 深度保护
maxDepth 不只作用于请求体。当 axios 用 AxiosURLSearchParams 序列化 params(GET 查询参数)时,调用的是同一个递归遍历器。调用链为:lib/helpers/buildURL.js → lib/helpers/AxiosURLSearchParams.js → toFormData(params, this, options):
function AxiosURLSearchParams(params, options) {
this._pairs = [];
params && toFormData(params, this, options); // options 即 paramsSerializer
}
在 lib/helpers/toFormData.js 中定义了默认深度上限:
// Default nesting limit shared with the inverse transform (formDataToJSON) so
// the FormData <-> JSON round-trip stays symmetric.
export const DEFAULT_FORM_DATA_MAX_DEPTH = 100;
递归构建过程中每一层都会检查深度(lib/helpers/toFormData.js、L246-L249):
function throwIfMaxDepthExceeded(depth) {
if (depth > maxDepth) {
throw new AxiosError(
'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
);
}
}
超出限制时抛出 code 为 ERR_FORM_DATA_DEPTH_EXCEEDED 的 AxiosError(常量定义见 lib/core/AxiosError.js),而不是任由调用栈溢出导致进程崩溃。
如果你的 params 对象合理地嵌套超过 100 层,可以显式调高上限:
// Raise the limit if your params object legitimately nests deeper than 100 levels:
axios.get('/api', { params: deepObject, paramsSerializer: { maxDepth: 200 } });
安全提示:仅在数据结构确实需要时才调高
maxDepth。默认值 100 能保护「把客户端可控数据转发为 axiosparams」的服务端代码,抵御通过深度嵌套对象发起的拒绝服务(DoS)攻击。同样的机制也通过build()中的循环引用检测(stack.indexOf(value) !== -1时抛出Circular reference detected)覆盖自引用对象。
请求体一侧对应的是 formSerializer 选项:transformRequest 中 toURLEncodedForm(data, formSerializer) 会把 formSerializer 原样透传给 toFormData(见 lib/defaults/index.js),因此需要为深层嵌套的请求体调高上限时,写法为:
axios.post(url, deepData, {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
formSerializer: { maxDepth: 200 },
});
此外,toFormData 支持 metaTokens(默认 true)、dots(默认 false)、indexes(默认 false)等选项;{} 后缀的键会触发带深度限制的 JSON.stringify,dots: true 时键名改用点号风格,indexes 可控制数组下标的渲染方式。这些选项同样可通过 formSerializer / paramsSerializer 透传。
七、三种方案选型小结
| 场景 | 推荐方案 | 依据 |
|---|---|---|
| 现代浏览器 + Node v10+ | 直接传 URLSearchParams 实例 |
标准 API,axios 自动设置 Content-Type(lib/defaults/index.js) |
| 旧浏览器/无 URLSearchParams 环境 | qs.stringify + 显式 Content-Type |
字符串数据原样透传 |
| 直接传 JS 对象(含嵌套) | postForm/post + content-type: application/x-www-form-urlencoded |
v0.21.0 起自动序列化,支持 maxDepth 等选项 |
| Node 老版本 | querystring |
已废弃(Node v16 起),仅存量代码使用 |
完整文档另见英文原文 docs/pages/advanced/x-www-form-urlencoded-format.md 及中文版本 docs/zh/pages/advanced/x-www-form-urlencoded-format.md。相关测试可参考 tests/unit/axios.test.js、tests/unit/helpers/AxiosURLSearchParams.test.js 与 tests/unit/toFormData.test.js,其中覆盖了数组、嵌套对象序列化及深度上限的行为验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00