省流版
如果通过multipart/form-data
类型上传file时,记得在Content-Disposition
加 filename
前言
事情起因于我想写一款基于picgo的上传插件,本想着上传也就是洒洒水的事情….没想到发现了问题
picgo的插件开发
因为不是本次的重点,这里简单提几句,picgo的插件运行环境为electron
也就是node.js
,所以下文涉及到picgo的运行环境都可以视为node.js
nodejs中的axios multipart form-data上传
在node.js环境中不像浏览器可以直接new FormData
,而是需要采用新的库来进行模拟form data 安装后的上传代码就很简单了,跟平时在浏览器环境中用formdata没啥太大差别
const formData = new FormData()
formData.append('a', 'b')
axios.post('https://httpbin.org/post', formData);
picgo封装的axios
新版的picgo底层用了axios来做支持,那么看了一下picgo官方文档,想着跟axios没啥差别 就直接代码复用了,结果发现了一些不一样的事情…
首先在我输入属性的时候,vscode联想出现了一个名为formData的字段联想…我看了一下axios和picgo本身的官方文档,是没有formData的属性的,好奇就挖了一下picgo-core的源码
import FormData from 'form-data'
// 省略一部分代码
if ('formData' in options) {
const form = new FormData()
for (const key in options.formData) {
const data = options.formData[key]
appendFormData(form, key, data)
}
opt.data = form
opt.headers = Object.assign(opt.headers || {}, form.getHeaders())
__isOldOptions = true
// @ts-expect-error
delete opt.formData
}
发现picgo封装的axios底层也是一样用formdata来处理,只不过多了个form.getHeaders
,这个方法的意思下面来说。
nodejs+nest中处理图片上传
nest默认用的http框架是express,那么基于express的multer封装了nest专属的装饰器:docs.nestjs.com/techniques/…
const storage = multer.diskStorage({
destination: function (req, file, cb) {
console.log('destination');
// 省略处理路径逻辑
cb(null, userPath);
},
filename: function (req, file, cb) {
console.log('filename');
// 省略处理文件名逻辑
cb(null, fileInfo.name + '-' + Date.now() + fileInfo.ext);
},
});
@Post('/upload')
@UseInterceptors(FileInterceptor('filepond', { storage }))
async upload(@UploadedFile() file: Express.Multer.File, @Req() req) {
const fileInfo = path.parse(req.file.filename);
}
问题复现
后端写好后,开始写picgo的插件上传代码
const formData = new FormData()
formData.append('filepond', imgList[0].buffer)
const res = await ctx.request({
url: `${userConfig.host}/upload`,
method: 'POST',
data: formData.getBuffer(),
resolveWithFullResponse: true,
headers: {
utoken,
...formData.getHeaders(),
'Content-Length': formData.getLengthSync()
}
})
这里多了几个方法
formData.getBuffer
为获取formdata最终转换为buffer,用了此方法就不能添加流类型的value。
formData.getHeaders
为生成multipart/form-data
的http请求头定义
formData.getLengthSync
也是生成http请求头定义的值
感谢form-data和开源世界,能够非常方便的获取到所有需要的信息,那么这样就成功了吗?运行上面的代码后会发现后台怎么也拿不到文件,也没有报错,唯一的报错就是
// error info
Cannot read properties of undefined (reading 'filename')
// backend code
const fileInfo = path.parse(req.file.filename);
对比我们写的后端代码+打断点可以很快推断出req.file
为undefined
第一次尝试解决
- 更换form-data实现插件,采用form-data-node x 失败
- 升级和降级nest中multer的版本 x 失败
通过简单版本升级的方式不能解决下面的问题后..我开始尝试了写浏览器版本的上传代码…发现竟然能够上传成功…于是我就开始抓包picgo,进行http请求分析
抓包分析
浏览器抓包非常简单,f12打开开发者工具即可,picgo抓包的话 由于axios支持proxy字段,所以只需要把此字段的地址指向我们抓包代理地址即可。省略抓包流程,得到的数据如下(下文省略一些其他的http头定义,只展示最关键的头定义)
浏览器
http header
Content-Length:
250653
Content-Type:
multipart/form-data; boundary=----WebKitFormBoundaryyEztrYE9bZYrTrnQ
http body
------WebKitFormBoundaryyEztrYE9bZYrTrnQ
Content-Disposition: form-data; name="filepond"; filename="1680600398864.jpeg"
Content-Type: image/jpeg
picgo/node.js
http header
Content-Type: multipart/form-data; boundary=--------------------------052948472259119770453321
Content-Length: 250525
http body
----------------------------052948472259119770453321
Content-Disposition: form-data; name="filepond"
Content-Type: application/octet-stream
对比两者的区别,发现有三处不同
- header Content-Type值不同
- body Content-Type值不同
- body Content-Disposition 属性不同
搜集资料可以了解到
header Content-Type值不同
是因为需要生成一个随机值,这个随机值与body中定义到分割随机值一致即可。
body Content-Type值不同
是因为传递到文件类型不同,也就是mime type
上述的值都常规的定义,看起来好像不是关键点
最关键点也就是body Content-Disposition
两者少了一个filename的属性,在浏览器中包含,在nodejs中到代码不包含… 我开始意识到问题到所在了 于是我改动了一下picgo插件中的代码
const formData = new FormData()
formData.append('filepond', imgList[0].buffer, { filename: 'xxx.png' })
再次运行代码后发现后端能够正常接受到file… 心态有点崩裂…
为什么会出现此问题
我开始找filename字段的定义 rfc1867
File inputs may also identify the file name. The file name may be
described using the ‘filename’ parameter of the “content-disposition”
header. This is not required, but is strongly recommended in any case
where the original filename is known. This is useful or necessary in
many applications.
大意上明白 这不是一个必传到属性,但是规范定义还是建议传递… 那么…为什么nodejs中会拿不到值呢?我开始去挖multer
的实现
// lib/make-middleware.js
try {
busboy = new Busboy({ headers: req.headers, limits: limits, preservePath: preservePath })
} catch (err) {
return next(err)
}
req.pipe(busboy)
发现multer
也是用了Busboy
于是我去看Busboy
的实现
if (disp.params) {
if (disp.params.name)
partName = disp.params.name;
if (disp.params['filename*'])
filename = disp.params['filename*'];
else if (disp.params.filename)
filename = disp.params.filename;
if (filename !== undefined && !preservePath)
filename = basename(filename);
}
====
if (partType === 'application/octet-stream' || filename !== undefined) {
// File
this.emit(
'file',
partName,
this._fileStream,
{ filename,
encoding: partEncoding,
mimeType: partType }
);
}
简单来看,大意是,如果没有filename,但是类型为application/octet-stream
还是认定你为文件的,并且抛出自定义事件 这个没啥问题.. 于是回头继续看multer
终于发现了问题
busboy.on('file', function (fieldname, fileStream, filename, encoding, mimetype) {
// don't attach to the files object, if there is no file
if (!filename) return fileStream.resume()
非常简单的认定 如果没有filename 那么就不是文件… 当然每个后端的http框架处理方式不一致,有些可能会给你自动命名,有些可能不会…
那么浏览器中就不会出现此问题吗?其实也会引发此问题,如果你formdata中append的是一个blob/buffer而不是一个file呢?
结尾:如果类型为file 记得加 filename