Axios升级后Content-Type从JSON变表单?排查过程与二次封装实战
发布时间:2026/9/28 7:44:56 锦皓数字建站

前端项目里axios 大概是默认选项了。为什么这次单独想把它拎出来写一篇因为最近在做一次版本升级的时候踩了个坑升级前浏览器发送的报文是 JSON升级完 axios 之后再发同样的请求Content-Type 居然变成了表单模式后端直接返回 415接口全部联调失败。排查的过程比较曲折最后发现是对 axios 的 content-type 自动判断逻辑理解不够。这篇文章把整个排查过程、axios 的 content-type 机制、multipart 上传、二次封装这些实战内容串起来聊一遍如果你也在做 axios 升级或者准备二次封装这篇应该能帮你省下不少排查时间。1. Axios 升级引发的 Content-Type 异常从 JSON 变成表单模式1.1 问题现象升级前后报文对比先说说我当时遇到的现象。项目原本用的 axios 0.21.4某个版本需求里依赖了一个新 SDK顺手就把 axios 升到了 1.6.x 版本。升级后运行回归测试某几个 POST 接口开始报错后端日志显示请求体解析失败返回 415 Unsupported Media Type。打开浏览器 Network 面板对比报文差异非常明显。升级前的请求长这样POST /api/user/save HTTP/1.1 Content-Type: application/json;charsetUTF-8 {username:tom,age:18}升级后同一段代码发出去的请求变成了POST /api/user/save HTTP/1.1 Content-Type: application/x-www-form-urlencoded;charsetUTF-8 usernametomage18注意业务代码一行没动设置请求头的姿势也没变后端接口更没换。问题就是 axios 内部对数据序列化方式的处理在新版本里走了不同路径。1.2 根因分析axios 版本差异与默认行为变化axios 1.x 重写了部分核心逻辑尤其是对请求数据的序列化判断更严格。升级前 0.x 版本里很多场景下即使 data 类型不标准axios 也会依据 headers 里已有的 Content-Type 来做兜底或者默认走 JSON 序列化。升级到 1.x 之后axios 采用了一套更明确的 data 类型检测策略如果你的 data 不是普通对象而是其他形态它就会用对应的序列化方式覆盖掉你设置的 Content-Type。具体来说axios 1.x 的 transformRequest 逻辑里对URLSearchParams实例会主动序列化为keyvaluekey2value2形式并设置application/x-www-form-urlencoded。如果你在做二次封装时为了兼容某些老接口用URLSearchParams包装过 POST 请求的参数新版本就会直接把它转成表单模式。还有另一种情况如果拦截器里对 data 做了qs.stringify处理也会触发同样的路径。1.3 解决方案显式指定 Content-Type定位到根因后改法其实很直接。在发送 POST 请求时显式指定 Content-Type 并确保 data 是普通对象即可。axios.post(/api/user/save, { username: tom, age: 18 }, { headers: { Content-Type: application/json } })如果你封装的请求工具里统一处理了 data可以像下面这样在封装层强制 JSON 化service.post function(url, data {}, config {}) { return instance.post(url, data, { ...config, headers: { Content-Type: application/json, ...(config.headers || {}) } }) }这里有个细节headers 的展开顺序很重要。如果你把config.headers放在前面调用方传进来的 headers 可能会覆盖掉你设置的 JSON 类型导致问题依旧。所以一定要把内置的 Content-Type 放在后面作为兜底同时允许调用方显式覆盖。注意如果你把 data 预先序列化成了 JSON 字符串比如JSON.stringify(data)那 axios 1.x 不会自动帮你设置 Content-Type因为字符串类型的 data 默认被视为“已经序列化完成”这种情况下也必须手动设置application/json。2. Content-Type 机制深度拆解axios 到底怎么判断数据格式2.1 axios 的 data 类型检测策略axios 在发送请求前会经过一个 transformRequest 过程内部根据 data 的形态决定如何序列化以及是否修改 Content-Type。判断顺序大概是这样的data 类型序列化行为Content-Type 自动设置普通对象isPlainObject转为 JSON 字符串application/jsonURLSearchParams 实例转为 query stringapplication/x-www-form-urlencodedFormData 实例不序列化原样发送浏览器环境自动带 multipart/form-data; boundaryArrayBuffer / Blob / Stream原样发送不自动设置字符串原样发送不自动设置官方文档里其实写得比较隐晦很多人只看使用示例根本没有留意这套自动判断逻辑。实际开发中出问题的往往不是普通对象而是绕过了检测路径的“特殊类型”。举个最常见的例子很多人会在 POST 请求里传一个已经 JSON.stringify 过的字符串因为在某些场景下后端要求字符串格式的 JSON但这样操作后 Content-Type 不会自动变成 application/json而是沿用默认或者被改成 text/plain后端自然就解析不了。2.2 表单模式与 JSON 模式的适用场景JSON 模式现在基本是前后端交互的主流因为结构清晰、层级丰富适合复杂嵌套数据。而表单模式application/x-www-form-urlencoded常见于传统 Web 接口、老系统对接、以及某些不想上 JSON 解析的网关服务。表单模式的特点是数据扁平化只能表达keyvalue的简单结构深层嵌套会很难处理。比如一个数组字段表单模式下只能用tags[0]atags[1]b这种约定来表达后端如果没做相应解析很容易出问题。所以当你发现 axios 突然走了表单模式除非后端本来就是按表单格式设计的否则大概率是 bug。2.3 配置技巧与全局默认策略与其每次请求都手动设置 Content-Type不如在创建实例时就把默认策略定好。我这里给一个比较稳健的配置方案const instance axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000, headers: { Content-Type: application/json } }) instance.interceptors.request.use(config { // 统一处理 GET 参数序列化 if (config.method get config.params) { config.params removeEmptyParams(config.params) } // 如果 data 是 FormData删除 Content-Type让浏览器自动带 boundary if (typeof FormData ! undefined config.data instanceof FormData) { delete config.headers[Content-Type] } return config })这里有一个非常关键的点当 data 是 FormData 时绝对不要手动设置Content-Type: multipart/form-data因为 multipart 请求头必须带一个随机生成的 boundary 字符串你手动设置却没有 boundary或者 boundary 和请求体不一致后端会直接报错。正确做法是删掉 Content-Type交给浏览器自动生成完整请求头。3. Axios Multipart 文件上传实战从单文件到多文件到进度监听3.1 单文件上传的正确姿势文件上传是 axios 用得最多的场景之一很多人都知道要传 FormData但细节上还是会翻车。先看标准写法const uploadFile (file) { const formData new FormData() formData.append(file, file) formData.append(bizType, avatar) return instance.post(/api/file/upload, formData, { // 不能手动设置 Content-Type原因上面已经说过 }) }重点FormData 的 key 名要和后端接口约定的字段名完全一致否则后端拿不到文件。很多同学后端明明写的RequestParam(file)前端却 append 成filedata这种低级错误还不少。3.2 多文件上传与进度监听多文件上传有两种常见形态一是多个独立字段二是同一字段传多文件数组。前者多次 append 不同 key后者使用同一个 key 循环 append。const uploadMultiFiles (files) { const formData new FormData() files.forEach(file { formData.append(files, file) }) return instance.post(/api/files/upload, formData, { onUploadProgress: (progressEvent) { const percent Math.round((progressEvent.loaded * 100) / progressEvent.total) console.log(上传进度, percent %) } }) }后端接口设计上如果是 Spring Boot 用MultipartFile[]接收字段名就是files如果用ListMultipartFile写法上略有差异但前端字段名也要一致。进度监听是我建议每个上传功能都加上的尤其是大文件场景。axios 的onUploadProgress回调能拿到loaded和total做进度条数据源完全够用。这里有个坑文件上传是二进制流progressEvent.total在部分浏览器或者跨域场景下可能为 0导致计算百分比出现 NaN。要做一层兜底const percent progressEvent.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 03.3 与后端对接的注意事项文件上传的坑往往不在前端而在前后端约定不清晰。以下几个点需要特别注意第一字段名不一致问题。建议前端把接口文档里的字段名作为常量抽出来不要散落各个业务页面里。第二额外业务参数放在文件字段后面。FormData 的 append 顺序在绝大多数后端框架里不敏感但某些严格校验的网关会要求文件字段放在最后。第三axios 1.x 在 Node.js 环境比如 SSR、单元测试处理 FormData 的逻辑和浏览器端有差异。Node 端 axios 需要特殊的 form-data 包来生成 boundary如果你在 Node 环境发文件请求报错先检查依赖是否完整。4. Axios 二次封装最佳实践项目级的请求层应该长什么样4.1 为什么要封装不封装行不行很多小项目初期图省事直接全局用axios.post裸调等业务复杂起来就痛苦了每个页面都要重复设置 token、处理超时、统一错误弹窗后端一旦调整状态码规范你得全项目搜response.data.code去改。二次封装的核心目的就是把这些横切逻辑收敛到一个地方业务侧只说人话。4.2 实例创建与请求拦截器先看一个我常用的封装结构分为四个文件request.js、api.js、modules目录、错误处理工具。核心的request.js长这样import axios from axios import { Toast } from /ui import { getToken, clearToken } from /auth const instance axios.create({ baseURL: /api, timeout: 15000, headers: { Content-Type: application/json } }) instance.interceptors.request.use( config { const token getToken() if (token) { config.headers[Authorization] Bearer ${token} } return config }, error Promise.reject(error) )请求拦截器里最常见的两件事拼 token 和统一处理请求参数。token 拼到请求头里注意不要覆盖掉已有的 Authorization如果某些接口使用独立鉴权头的话。4.3 响应拦截器与业务错误处理响应拦截器是二次封装的灵魂几乎所有的业务规则都在这层体现。我习惯把所有接口约定为统一格式{ code, message, data }code 为 0 时代表成功非 0 则代表业务失败。instance.interceptors.response.use( response { const res response.data if (res.code ! 0) { Toast.error(res.message || 业务处理失败) return Promise.reject(new Error(res.message)) } return res.data }, error { const status error.response?.status if (status 401) { clearToken() window.location.href /login return Promise.reject(error) } if (status 403) { Toast.error(没有操作权限) } else if (error.code ECONNABORTED) { Toast.error(请求超时请稍后重试) } else if (!error.response) { Toast.error(网络异常请检查网络连接) } else { Toast.error(error.response.data?.message || 请求失败(${status})) } return Promise.reject(error) } )如果你的项目登录态校验不靠 401 而是靠业务 code那可以把这个分支移入业务错误处理里。这里我的习惯是HTTP 层错误走 error 回调业务错误走 code 判断各管各的互不混淆。4.4 取消重复请求与请求重试二次封装进阶玩法就多了这里分享两个我实际项目里用到过的。第一个是重复请求拦截。用户在表格页疯狂点击查询按钮瞬间发出好几个相同请求既浪费带宽又可能造成数据覆盖。解决方案是维护一个请求 Map以method url params作为 key请求发起前先检查是否存在相同请求存在则取消旧的。const pendingMap new Map() const getRequestKey (config) { const { method, url, params, data } config return [method, url, JSON.stringify(params), JSON.stringify(data)].join() } const addPending (config) { const key getRequestKey(config) const controller new AbortController() config.signal config.signal || controller.signal if (!pendingMap.has(key)) { pendingMap.set(key, controller) } }axios 1.x 原生支持AbortController比老版本里依赖CancelToken更现代新项目直接用这个方案。第二个是接口失败重试。适用于网络抖动、服务重启等瞬时不稳定场景。注意重试必须只在幂等接口上做POST 类新增接口重试可能造成重复数据。重试次数一般 1-2 次每次间隔递增避免雪崩。5. 常见问题快查表与避坑指南5.1 问题快查表问题现象可能原因解决方案升级后 Content-Type 变成 application/x-www-form-urlencodeddata 是 URLSearchParams 实例或拦截器做了 qs.stringify改用普通对象或显式指定 application/json手动设置 multipart/form-data 后上传报错缺少 boundary 导致请求头不完整删除 Content-Type让浏览器自动生成入参是 JSON 字符串但后端解析不到字符串类型不会自动设置 application/json手动设置 Content-Type 或直接传对象上传进度百分比 NaNprogressEvent.total 为 0做参数兜底total 不存在时给 0请求超时但错误提示不统一超时错误和网络错误混在一起在响应拦截器里单独判断error.code ECONNABORTED401 后页面跳转延迟或重复跳转多个请求同时 401加一个跳转锁避免重复执行 location.href5.2 我踩过的坑和心得第一次遇到 content-type 问题时我甚至在浏览器 Network 面板里对比了半个小时的报文还以为是后端网关做了拦截最后才发现是URLSearchParams在作祟。这个坑给我的教训是升级第三方库不能只看 changelog关键模块的默认行为变化必须做兼容测试。另外关于二次封装有个很多人忽略的点拦截器里不要随意修改config.headers的引用。axios 内部对 headers 做了多层合并你如果在拦截器里直接config.headers something可能会丢失 AxiosHeaders 原型上的方法导致某些版本下出现怪异行为。正确做法是config.headers.set(key, value)或者只修改已有属性。最后想说axios 这个东西虽然看起来简单但它内部对 data 类型、content-type、适配器环境的行为判断相当精细。你在开发时把 Network 面板的报文当成调试的基础遇到任何问题先看请求头、请求体、响应头八成以上的问题一眼就能发现原形。根据我个人经验把 content-type 机制理解透再配合良好的二次封装前端请求层能少踩一半的坑。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。