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 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
ShineWinsu3 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说4 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
程序员老赵6 小时前
Docker 部署青龙面板:轻松搭建定时任务管理平台
前端·docker·开源
Swot6 小时前
Asciidoctor 公式数字下标错位问题:原因分析与解决方案
前端
Swot6 小时前
leo-editor 中将 Markdown 内容转成 asciidoc 格式
前端
狒狒AI6 小时前
Remotion / GSAP+Three / HyperFrames / Skia 四栈实测:代码渲染知识视频的选栈方法(附开源技能)
前端·人工智能·aigc
修炼的dance6 小时前
浏览器里导出H.265,8.5秒和55.5秒差在硬件
前端
补码缩写6 小时前
WebCodecs选编码器,探测之后还要验解码和播放
前端
大文说跨境6 小时前
亚马逊多账号管理浏览器怎么选?云登/紫鸟/AdsPower 对比
java·前端·自动化