关于multipart form-data中filename的问题

省流版

如果通过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.fileundefined

第一次尝试解决

  1. 更换form-data实现插件,采用form-data-node x 失败
  2. 升级和降级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

对比两者的区别,发现有三处不同

  1. header Content-Type值不同
  2. body Content-Type值不同
  3. 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

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYmSsn7H' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片