Skip to content

利用fetch上传文件 #237

Open
Open
@yaofly2012

Description

@yaofly2012

背景

最近负责的一个项目里(接口请求采用fetch)涉及文件上传表单,参考另外一个项目(接口请求采用axios)的操作方式,顺理成章写好了(Ctrl+C/V):

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);

  return fetch('https://example.com', {
    method: 'POST',
    body: formData,
    headers: {
        'Content-Type': 'multipart/form-data'
    }
  })
}

结果上传失败,后端童鞋说请求有问题(没有Boundary)。其他项目好好地,为啥我这个项目就不行(惊悚)。
先检查了下请求:
image
content-type里确实没有boundary

解决问题

删除手动设置的Content-Type就行了:

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);

  return fetch('https://example.com', {
    method: 'POST',
    body: formData
  })
}

观察下此时的请求(content-type里有boundary):

content-type: multipart/form-data; boundary=----WebKitFormBoundaryaUiVhiUXfFmuYUEc

image

解惑

1. 什么是FormData boundary?

得先看看Form Content-Type的值之一multipart/form-data的功能。

The content type "application/x-www-form-urlencoded" is inefficient for sending large quantities of binary data or text containing non-ASCII characters.

所以才引入了multipart/form-data,而bounday在作为各个part(对应表单的各个字段)之间的分隔符。

const formData = new FormData();
    // Form有三个字段
    formData.append('file', file);
    formData.append('name', 'john');
    formData.append('age', 22);

    return fetch(
        'https://example.com', 
        {
            method: 'POST',
            body: formData
        }
    )

看看请求数据:

------WebKitFormBoundaryy7AV3mXGZkpXR6dr
Content-Disposition: form-data; name="file"; filename="air.detail.png"
Content-Type: image/png


------WebKitFormBoundaryy7AV3mXGZkpXR6dr
Content-Disposition: form-data; name="name"

john
------WebKitFormBoundaryy7AV3mXGZkpXR6dr
Content-Disposition: form-data; name="age"

22
------WebKitFormBoundaryy7AV3mXGZkpXR6dr--

顺便看看请求头:

content-type: multipart/form-data; boundary=----WebKitFormBoundaryy7AV3mXGZkpXR6dr

三个表单字段对应三个part。关于每个part的构成可以看下规范。文件上传对应的part会多个filename,并且内容是字节流也不会展示在里面。
正因为boundary浏览器控制台面板里可以看到格式化后的FormData数据:
image

2. 为什么需要删除手动设置的Content-Type

Disappointing but true.

浏览器会自动设置'Content-Type': 'multipart/form-data',并且计算bundary。手动设置'Content-Type': 'multipart/form-data'时,浏览器就不会自动处理了。

3. 为什么axios可以手动设置Content-Type

axios是基于Ajax的,难道是Ajax可以手动设置Content-Type?试着把上面的代码换成Ajax也是不能上传成功,说明Ajax上传文件时也不能手动设置Content-Type!看来axios有点“智能”。撸下axios代码发现了原因:
axios/lib/adapters/xhr.js

if (utils.isFormData(requestData)) {
    delete requestHeaders['Content-Type']; // Let the browser set it
}

再看看isFormData的实现

function isFormData(val) {
  return (typeof FormData !== 'undefined') && (val instanceof FormData);
}

原来axios会内部会自动删除FormData的手动设置的Content-Type

总结:

  1. 无论是Ajax还是fetch,在使用FormData传输数据时,都不能手动设置content-type
  2. axios会内部会自动删除FormData的手动设置的Content-Type

参考

  1. WebAPI: fetch
  2. Uploading files using 'fetch' and 'FormData'
  3. 踩坑篇--使用 fetch 上传文件
  4. fetch - Missing boundary in multipart/form-data POST

扩展:

  1. MDN: Content-Disposition
  2. w3.org: Form content types
  3. What does enctype='multipart/form-data' mean?
  4. application/x-www-form-urlencoded or multipart/form-data?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions