前言
前段时间遇到了一个很诡异的Bug,发送FormData类型数据的POST请求被浏览器把Content-Type自动设置为了application/json,导致后端无法解析出正确的数据来。在尝试解决这个问题的时候比较深入的调研了multipart/form-data类型数据的相关内容,所以有了这篇文章来记录分享一下。
multipart/form-data 简介
一般来说,前端在进行Post请求的时候会根据场景发送不同类型的数据,该Post请求头的Content-Type字段必须声明相应的数据类型,比如说application/x-www-form-urlencoded、application/json、multipart/form-data等,根据实际应用,有时候会需要请求时来手动设置,不过大部分情况下浏览器会根据Body中的数据来自发的设置相应的请求头。
POST请求时发送FormData类型的数据会将设置multipart/form-data,完整的设置如下:
Content-Type: multipart/form-data; boundary=xxxx
前半部分代表数据类型,而boundary代表分隔符,boundary对应的xxxx是由请求方自定义设置的。
一般来说常常在需要发送文件或者需要发送文件加文本的时候我们才会使用FromData类型的数据格式来发送,FromData格式的数据同样是通过键值对的形式来表现的,不过实际的消息格式如下
xxxx
Content-Disposition: form-data; name="foo"
bar
xxxx
Content-Disposition: form-data; name="baz"
The first line.
The second line.
xxxx
这个FormData类型的数据可以理解为有两个key,分别是foo和baz,他们的值用boundary分隔,这里就是xxxx。
表单请求
一般来说,表单提交是以前最常用的一种发送方式,将<form></form>
标签中的enctype设置为multipart/form-data
就可以发送,我们常常是在提交带有文件的表单时使用这种格式的数据。
<form action="example.com" method="post" enctype="multipart/form-data">
<fieldset>
<legend>Registration example</legend>
<p>
First name: <input type="text" name="firstname" /><br />
Last name: <input type="text" name="lastname" />
<input type="file" name="file" />
</p>
<p>
<input type="submit" />
</p>
</fieldset>
</form>
在这种表单提交的情景下,我们不需要手动设置Content-Type,所以也不用去管Body中的数据是如何排列的,不需要知道boundary是什么,这些都有浏览器来自动完成。
使用ajax和FormData
现代的前端开发已经很少会用纯表单提交的方式了,表单一般只是用来获取数据,比如说用<input type="file" />
来获取文件数据,然后实例化一个FormData类型的对象来存储这些数据,然后通过ajax或fetch来进行http请求。
const file = document.querySelector('#file')[0]
const formData = new FormData()
formData.append('file', file)
formData.append('firstName', 'Harlan')
formData.append('LastName', 'Zhang')
http.post('example.com', formData)
这里尤其注意的是,我们不需要设置Content-Type,因为浏览器会自动设置。如果我们手动设置了Content-type,会覆盖掉浏览器自己的设置,而因为我们不知道formData对象里面的boundary分隔符是什么,所以就会导致后端接受到数据以后在Content-type中找不到boundary或者boundary的值与formData中的boundary不一致,导致无法获取正确的数据。
不使用FormData类型来进行ajax请求
这里就要提到我一开始提到的Bug了,公司自己开发的H5页面,在IOS系统的钉钉内置的浏览器中发生了这个问题。浏览器会自动把发送FormData类型的数据的Post请求的Content-Type设置为application/json,导致后端无法解析出正确的数据来。这里我尝试手动来设置请求头,但就像上文说的那样,想尽办法都拿不到formData对象里面的boundary,后来决定Body中不使用FormData格式的数据,而是直接用字符串来模拟FormData真正的存储数据的格式,如下:
xxxx
Content-Disposition: form-data; name="foo"
bar
xxxx
Content-Disposition: form-data; name="baz"
The first line.
The second line.
xxxx
模拟这种类型的字符串,然后将其放到body中发送出去,boundary同时也可以自己定义。
其中比较麻烦的一步就是File类型的数据该如何转化,这里需要使用FileReader
来读取出File类型数据的内容来,这里独处来的数据是Buffer类型的,然后将其转化成String写进这个模拟FormData的字符串中。
最后,将这个模拟FormData的字符串放到Body中然后使用Post请求发出,此时的POST请求我们就可以手动设置Content-Type,其中boundary的值我们也已经知晓了。
至于具体的实现,大家可以参考MDN上的示例,这里有比较完整的源码。
总结
说实话,自己模拟FormData类型的数据比想象中的要困难,这其中的难点主要还是在对File类型数据的处理上,如果数据比较大就必须分片来装换,否则就会超出变量的存储极限,有点大文件分片上传的意思。而且其中还遇到了png等格式的文件转换后后端依旧无法识别的现象。这篇文章主要还是记录分享一下multipart/form-data这种类型的Post请求的一些细节。