FormData 传递 JSON 数据的问题解决

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

一、问题背景

1.1 接口需求

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

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

1.2 常规做法

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

  1. 修改请求头中的 Content-Typemultipart/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 只能是:stringBlob (包括子类型,如 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 向正确的方向排查解决。

参考资料

相关推荐
Data_Journal11 小时前
【无标题】
大数据·服务器·前端·数据库·人工智能
我爱加班、、11 小时前
new Map()+Array.from()整理elementPlus的级联器数据
linux·前端·javascript
Hx_Ma1611 小时前
Map集合的5种遍历方式
java·前端·javascript
css趣多多11 小时前
render函数
前端·javascript·vue.js
web打印社区11 小时前
前端开发实现PDF打印需求:从基础方案到专业解决方案
前端·vue.js·react.js·electron·pdf
时光追逐者11 小时前
使用 MWGA 帮助 7 万行 Winforms 程序快速迁移到 WEB 前端
前端·c#·.net
搬砖的阿wei11 小时前
CSS常用选择器总结
前端·css
Trae1ounG12 小时前
Vue Iframe
前端·javascript·vue.js
阿部多瑞 ABU12 小时前
`tredomb`:一个面向「思想临界质量」初始化的 Python 工具
前端·python·ai写作
比特森林探险记12 小时前
React API集成与路由
前端·react.js·前端框架