问题描述
需求是上传一张图片到我们的对象存储服务器。
对方给了接口协议
- post 请求
- Content-Type 为 multipart/form-data
- form-data, key = "filecontent",value = 图片内容
之前自己用 node.js 写过,几行代码搞定,以为没啥问题,结果用 Golang 遇到了坑
node.js
node.js 发送请求,只需要这几行
const options = { method: "POST", url: "xxxxxxxxxxxxx", headers: { "Content-Type": "multipart/form-data" }, timeout: 3000, formData: { // key:filecontent value:stream "filecontent": fs.createReadStream(zip_file_path) } }; request(options, function (err, res, body) { if (err) { console.log(err); } else { let data = JSON.parse(body); if (data.errcode) { console.log(data) } else { console.log('cdn链接:\t' + data.download_url); } } });
golang
当需要在 golang 上发送这个请求的时候,就遇到了各种问题
网上搜到了一些方法,诸如,multipart.createFormFile 等等,关键是还是不对。
先看看 golang 的 net/http 包自带的方法,如何发一个 post 请求,如何设置 Content-Type
buf := new(bytes.Buffer)w := multipart.NewWriter(buf)http.NewRequest("POST", GIFT_URL, buf)contentType := w.FormDataContentType()req.Header.Set("Content-Type", contentType)
但是,你会发发现,即使你设置了你的 Content-Type 是 multipart/form-data,debug 也能看到你的 body 里面有这个文件的所有数据,但是对方服务器还是不一定能解析出来,完全看服务端的同学怎么实现、怎么解析。比如,服务器是一个对象存储系统,并且对上传的文件类型有严格判断,这样的话,大概率是错误的,为什么?
抓包看协议
先来看正确的请求
再来看一个出错的golang代码的请求
MIME
上图中可以看到,HTTP 协议之后,还带上了一个 MIME 协议。当我们的 HTTP 的 Content-Type 设置为 multipart/form-data 时,我们的请求后会跟着 MIME 协议。这是对于 HTTP 协议的扩展,通过搜资料,说是以前 HTTP 不能传送各种文件等等的东西,而 MIME 是邮件协议,对 HTTP 进行了扩展,让 HTTP 也可以传输各种文件等资源。
所以问题的本质是什么?
那既然抓包发现了 MIME,那么也很清晰的看到了 golang 发出的请求的错误原因,没有指定 MIME 的 Content-Type。Content-Type 类型很多,如下表格
类型 | 典型示例 |
---|---|
text | text/plain, text/html, text/css, text/javascript |
image | image/gif, image/png, image/jpeg, image/bmp, image/webp, image/x-icon, image/vnd.microsoft.icon |
audio | audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav |
video | video/webm, video/ogg |
application | application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf |
那我们上传的是图片,所以 Content-Type 为 image/xxx
神奇的 boundary 是什么
我们在抓到的包中还会看到有个叫做 boundary 的字符串,这个其实是一个分隔符,让我们能正确解析上传的文件
一个请求的具体信息大致如下
Content-Type: multipart/form-data; boundary=aBoundaryString(other headers associated with the multipart document as a whole)--aBoundaryStringContent-Disposition: form-data; name="myFile"; filename="img.jpg"Content-Type: image/jpeg(data)--aBoundaryStringContent-Disposition: form-data; name="myField"(data)--aBoundaryString(more subparts)--aBoundaryString--
每个字段/文件都被 boundary 分成单独的段,这样可以知道每个文件的范围。
Content-Disposition 又是什么
在常规的 HTTP 应答中, Content-Disposition 消息头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。
Content-Disposition 作为 multipart body 中的消息头时,第一个参数总是固定不变的 form-data;附加的参数不区分大小写,并且拥有参数值,参数名与参数值用等号(=)连接,参数值用双引号括起来。参数之间用分号(;)分隔,如上图。
修改一下我们的原始代码
那其实核心原因就是 MIME 的 Content-Type 了,我们的对象存储服务器,只接受一般的图片类型,要加上图片文件的类型,否则后端无法解析,就会报错,所以代码修改如下:
header := make(textproto.MIMEHeader)header.Set("Content-Disposition", fmt.Sprintf(`form-data;name="%s";filename="%s"`,"filecontent", baseFileName))mimeType := mime.TypeByExtension(ext)header.Set("Content-Type", mimeType)
加上 MIME 的 Type 即可。