先记住这个答案
fetch 的 body 传 FormData 实例时,浏览器自动设置 Content-Type 为 multipart/form-data,并附带随机 boundary 分隔各字段;手动设置会丢失 boundary,导致服务端解析失败。若传 JSON 字符串,需显式设置 headers 中的 Content-Type 为 application/json,否则服务端可能按纯文本处理。处理上,服务端需按对应 MIME 类型解析,前端还需注意序列化与 CORS 预检。
- FormData 自动生成 multipart boundary
- JSON body 必须手动声明为 application/json
- 手动给 FormData 设 Content-Type 会致解析失败
编码与头部差异的机制
当 fetch 的 body 选项传入 FormData 实例时,浏览器会将整个请求体重写为 multipart/form-data 编码,并在请求头自动附加 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXX。boundary 是随机生成的字符串,用于分隔每个表单字段的边界,服务端依赖它切分二进制与文本。因为头部和体强关联,任何手动设置的 Content-Type 都可能与真实 body 不匹配,所以标准做法是不写该头。
JSON 则不同,JSON.stringify 得到的字符串没有固有媒体类型,因此必须手动在 headers 中设置 Content-Type: application/json。若省略,许多服务端默认按 text/plain 或 application/octet-stream 处理,导致 req.body 为空或解析异常。同样,还需关注 charset,如 application/json; charset=utf-8,但通常只写主类型即可。
表单提交含文件与普通文本的工程场景
某后台管理页面需要一次性提交一个标题(字符串)、一张用户头像(Blob)和一组自定义元数据对象。若使用 JSON,必须先把二进制转为 base64 或 dataURL,会让体积增加约 33% 且后端解码复杂。改用 FormData:append('title', 'CEO'), append('avatar', fileInput.files[0]), append('meta', JSON.stringify(meta)),此时无需任何 Content-Type 设置,fetch 自动生成 multipart 头。
处理结果:请求头含 boundary,node.js 的 multer 或 busboy 能正确解析文件字段和文本字段;meta 需后端自行 JSON.parse。若误以为需要手动写 Content-Type: multipart/form-data,会发现丢失 boundary 的 400 错误。该场景的约束是文件与普通字段混发,选 FormData 能减少序列化逻辑,但代价是额外 multipart 开销。
手动设置失效的条件与替代
FormData 的手动 Content-Type 问题只在浏览器原生隔离下明显:你无法从外部拿到确切 boundary,因此设置一个不匹配的静态头必然破坏数据包。有些测试库模拟 fetch 时允许覆盖,但真实浏览器允许设置却不会自动补充缺失的 boundary,导致头与体不匹配。若需要自定义头,必须完全自己构造 multipart 格式,或改用 Blob 的 type 属性,但仍可能不一致。
JSON 的边界在于服务端对缺失 Content-Type 的容忍度不同:某些框架(如 Express 的 express.json)要求严格匹配,否则可能返回 415 或无法解析。处理做法是在服务端同时支持两种解析,或在客户端统一约定;若无法控制服务端,必须显式设置 application/json。同时跨域时,application/json 会触发 CORS 预检请求,需要服务端允许该头,而 FormData 的简单请求不需预检(若 method 为 POST 且无自定义头)。
容易答错的地方
- 手动给 FormData 设置 Content-Type 头
- 错误认为 multipart/form-data 必须像 JSON 一样显式设置。实际上浏览器生成完整头部含 boundary,手动设置会丢失边界串,导致服务端无法分隔字段,请求体解析失败。正确做法是不动该头,交给 fetch 自动填充。
- JSON body 忘记设置 Content-Type
- 很多初学者只把 JSON.stringify 结果传入 body,却不加任何 headers。服务端无法识别为 JSON,req.body 可能为空或变成字符串。需要显式 headers: {'Content-Type': 'application/json'},且字符串必须是合法 JSON,否则后端 JSON.parse 抛异常。
面试官还会怎么问?
如何确认浏览器自动生成的 Content-Type 实际值?
在浏览器 DevTools 的 Network 面板查看请求头,能看到完整值如 multipart/form-data; boundary=----WebKitFormBoundaryABC。也可在 fetch 前用 Headers 对象截获,但最终 boundary 由浏览器在发送时自动生成,无法提前读取。
服务端如何区分收到的是 JSON 还是 FormData?
按 Content-Type 判断:application/json 走 JSON 解析,multipart/form-data 用表单解析库。若无头,多数框架按普通文本处理;若两种头都有,需优先级约定,推荐强制服务端只认一种。
FormData 也可以发送纯 JSON 字段吗?为什么还要用 JSON body?
可以,把 JSON 序列化后放进字段,但服务端需额外 parse,通信体积更大,且不方便直接表达嵌套结构。JSON body 结构直接,适合 API 交换,而 FormData 更利于文件上传和传统表单提交。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。