FormData 传递 JSON 数据的问题解决

最近在开发中遇到了一个关于 FormData 传递 JSON 数据的问题,后端一直报 500 错误,但后端自己拼接的 curl 却能正常请求。经过一番排查,发现问题的根源在于 FormData 中 JSON 串的类型没对齐。本文记录整个排查过程和解决方案。

一、问题背景

1.1 接口需求

有一个接口需要同时传递两个参数:

  • 一个文件(File 类型)
  • 一段配置信息(JSON 格式)

1.2 常规做法

一般对于包含文件的请求,通常是用 FormData 包裹一下,具体流程是:

  1. 修改请求头中的 Content-Type 为 multipart/form-data
  2. 实例化一个 FormData
  3. 通过 append 方法添加文件和 JSON 数据

按照这个思路,我的代码如下:

javascript 复制代码
const formData = new FormData();
formData.append('file', file); // 文件
formData.append('req', JSON.stringify(reqParams)); // JSON 配置

二、问题现象

请求发送后,后端返回 500 同时出现如下报错:

bash 复制代码
HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported

错误大概的意思是后端不支持当前请求的 Content-Type,即后端期望接收 application/json 类型的数据,但实际收到的是 application/octet-stream(二进制流),导致无法正确解析。

三、排查过程

3.1 检查项目请求配置

首先怀疑是项目的请求处理有特殊配置或者是后端对于文件的处理存在问题,之前就遇到过包含 FormData 的接口生成 Sign 签名出错的问题。但测试后发现,单独传递文件是正常的 ,说明请求处理没有问题。然而,一旦添加了 JSON 参数,就会报 500 错误,这让我怀疑问题可能出在 JSON 数据的处理上。

3.2 报错搜索

我在网上搜了一下,找到了相同的报错:Spring/Postman Content type 'application/octet-stream' not supported。这个问题中提到了在 Postman 中可以通过设置字段的 Content-Type 来解决,这让我怀疑到问题可能还是跟 Content-Type 的设置有关。

3.3 对比 curl 字符串

参考上面的解决方案通过 Postman 拼接了一段可以正常请求的 curl 命令,然后再把前端请求出错的的 curl 命令进行对比后发现:

Postman 的 curl(能通):

bash 复制代码
curl -F "file=@test.jpg" -F "req={\"key\":\"value\"};type=application/json" ...

前端的 curl(报错):

bash 复制代码
curl -F "file=@test.jpg" -F "req={\"key\":\"value\"}" ...

关键差异在于:Postman 的 JSON 后面有 ;type=application/json 这正好和 Stack Overflow 上提到的 Content-Type 设置问题有关。

3.4 理解问题本质

在 multipart/form-data 格式中,每个字段都可以指定自己的 Content-Type。Postman 的 curl 中,req 字段明确指定了 type=application/json,所以后端能正确识别这是一个 JSON 数据。

而前端的请求中,req 字段没有指定 Content-Type,FormData 会将字符串类型的数据默认设置为 application/octet-stream(二进制流),导致后端无法正确解析。

FormData 的 append 方法支持三种类型:

  • 字符串 :会被当作普通文本,默认 Content-Type 为 application/octet-stream
  • Blob:可以自定义 Content-Type
  • File:继承自 Blob,有明确的文件类型

当我们使用 JSON.stringify() 后得到的是字符串,FormData 无法知道这是 JSON 格式,所以会导致后端报错。

四、知识点补充

为了更好的清楚问题的解决方案,我们先补充一些必要的知识点。

4.1 FormData API

FormData.append() 方法的用法:

javascript 复制代码
formData.append(name, value);
formData.append(name, value, filename); // 当 value 是 Blob 或 File 时

参数类型限制:

  • value 只能是:string 或 Blob (包括子类型,如 File)
  • 字符串类型会被当作普通文本,无法指定 Content-Type

4.2 Blob 对象

Blob(Binary Large Object) 是 JavaScript 中表示二进制数据的对象。

构造函数:

javascript 复制代码
new Blob(blobParts, options)

参数说明:

  • blobParts:一个由 ArrayBuffer、ArrayBufferView、Blob、DOMString 等对象构成的数组
  • options:可选配置对象
    • type:MIME 类型字符串,如 'application/json'、'text/plain' 等

注意:第一个参数必须是数组,即使只有一个元素也要用数组包裹。

示例:

javascript 复制代码
const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
const jsonBlob = new Blob([JSON.stringify({ key: 'value' })], { type: 'application/json' });

4.3 multipart/form-data 中的 Content-Type

在 multipart/form-data 格式中,每个字段都可以有自己的 Content-Type。

格式示例

css 复制代码
Content-Disposition: form-data; name="key";filename="blob"
Content-Type: application/json

{...json data...}

Content-Type 在 curl 命令中,可以通过 ;type=application/json 来指定。后端框架(如 Spring Boot)在解析 multipart 数据时,会根据 Content-Type 来决定如何解析字段值,如果 Content-Type 不匹配,就会抛出 HttpMediaTypeNotSupportedException 异常。

五、解决方案

5.1 使用 Blob 包装 JSON 字符串

既然字符串无法指定 Content-Type,而 Blob 可以,那就用 Blob 来包装 JSON 字符串:

5.1.1 ❌ 报错方案:

javascript 复制代码
const formData = new FormData();
formData.append('req', JSON.stringify(reqParams));

在开发者工具中可以看到,使用字符串时,req 字段没有指定 Content-Type:

5.1.2 ✅ 正确方案:

javascript 复制代码
const formData = new FormData();
const reqBlob = new Blob([JSON.stringify(reqParams)], { type: 'application/json' });
formData.append('req', reqBlob);

使用 Blob 并指定 type: 'application/json' 后,req 字段的 Content-Type 正确设置为 application/json:

5.3 完整示例

javascript 复制代码
async function uploadFileWithConfig(file, config) {
  const formData = new FormData();
  
  // 添加文件
  formData.append('file', file);
  
  // 添加 JSON 配置(使用 Blob)
  const configBlob = new Blob([JSON.stringify(config)], { type: 'application/json' });
  formData.append('req', configBlob);
  
  // 发送请求
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
    // 可以不手动设置 Content-Type,浏览器会自动设置
  });
  
  return response.json();
}

六、总结

6.1 问题根源

问题的根源在于:FormData 中字符串类型的数据默认 Content-Type 为 字符串,而后端期望的是 application/json,导致类型不匹配。

6.2 解决方案

使用 Blob 对象 包装 JSON 字符串,并通过 type 属性指定 Content-Type 为 application/json,这样后端就能正确识别并解析 JSON 数据了。

6.3 经验教训

相关API的基础知识还需要加深理解,如 multipart/form-data 中可以单独设置 Content-Type,Blob可以指定设置JSON格式。

同时善用 AI 工具 在这次排查中起到了关键作用。刚出现问题的时候让 Cursor 解决,它只能反复地修改整个请求的 Content-Type,后来还是人工定位到是单个字段的 Content-Type 问题。最终通过询问 curl 中差异的 ;type=application/json 是什么意思,才引导 Cursor 给出了使用 Blob 的解决方案。这说明了解决问题时不能过多的依赖 Cursor 需要自己先排查问题引导 Cursor 向正确的方向排查解决。

参考资料

相关推荐
行者-全栈开发1 小时前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
计算机魔术师2 小时前
AMD砸82亿买下李飞飞,这次不卖芯片卖什么?
前端
hzxpaipai3 小时前
杭州企业官网怎么建设?从策划到上线的5步建站方法
运维·服务器·前端·网络
怕浪猫3 小时前
2026年金九银十,我面了22个前端
前端·javascript·面试
IT_陈寒3 小时前
Vue 这个响应式陷阱,我的头发都掉没了
前端·人工智能·后端
前端的日常3 小时前
一个人+AI做情侣食谱小程序,30天纯赚
前端·javascript·后端
庄园特聘拆椅狂魔4 小时前
Java 后端转全栈的第一课:从前端项目搭建到技术选
java·开发语言·前端
triumph_passion4 小时前
前端路由策略:Memory、Hash 与 History
前端
李剑一4 小时前
AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!
前端·vue.js
牧艺4 小时前
浏览器端 3D 看车重磅升级:从三款展车到可试驾的八车展厅
前端·three.js·交互设计