如何使用fetch和FormData提交multipart/form-data格式的POST请求

14

这是一个正常工作的CURL示例:

curl -X POST \
  <url> \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
  -F file=@algorithm.jpg \
  -F userId=<userId>

我正在尝试使用isomorphic-fetch来复制这个请求。

我尝试了以下代码:

const formData = new FormData();
formData.append('file', file);
formData.append('userId', userId);

return fetch(`<url>`, {      
  method: 'POST',
  headers: {
    'Content-Length': file.length
    'Authorization: Bearer <authorization token>',
    'Content-Type': 'multipart/form-data'
  },
  body: formData
})`

我使用fs.readFileSync来生成传递给FormDatafile

前面的例子返回401 HTTP状态码(未经授权)和一个错误消息,说明通过标头发送的令牌中嵌入的userId与从formData传递的userId不匹配。

因此,我的怀疑是到达REST API的FormData格式不正确。

问题可能与Content-Length标头有关,但我没有找到更好的计算方法(如果我不使用Content-Length标头,则会收到一个411 HTTP状态代码Content-Length标头缺失)。

这种情况可能是由于Content-Length标头中的不正确值而导致失败吗?

其他关于为什么失败或如何更好地调试它的建议?

如果需要进一步信息以澄清此问题,请随时询问。

更新

我尝试了form-data模块,以使用方法formData.getLengthSync()获取正确的Content-Length值。

然而,问题仍然存在(401错误HTTP状态码响应)。


2
删除Content-Type请求头,因为这需要由浏览器自动生成以包含多部分边界。我认为如果你删除了这个和Content-Length头,你就应该没问题了。 - idbehold
我已经尝试过这个方法,但没有成功。当我不发送Content-Length头部时,API会返回一个411错误的HTTP状态码:服务器拒绝接受没有定义Content-Length的请求 - rfc1484
尝试将Content-Length设置为12345。无论您上传到哪个服务器,它都设计得不太好。 - idbehold
同样不起作用,我真的不明白为什么在执行 fetch 请求时 Content-Length 标头似乎是必需的,但是当我通过 CURL 执行相同的请求而没有这个标头时,一切都正常工作,并且不会返回 411 错误 HTTP 状态码。 - rfc1484
不确定你是否解决了这个问题,但我认为你的问题在于你使用了“Content-Type”,而应该使用“Content-Disposition”。 “Content-Length”是不必要的。 - Eric J. Crammer
如果您手动设置内容类型,则需要边界字符串!请参阅此帖子。对于formData类型,最好不要指定这些标头。 - S.Serpooshan
2个回答

14

只需从代码中删除 Content-Length Content-Type 头,因为这些头将由浏览器自动设置。

如果您打开网络检查器,运行此代码片段并提交表单,则应该看到 Content-Length 已正确设置:

const foo = document.getElementById('foo')
foo.addEventListener('submit', (e) => {
  e.preventDefault()
  const formData = new FormData(foo)
  formData.append('userId', 123)
  fetch('//example.com', {
    method: 'POST',
    body: formData
  })
})
<form id="foo">
  <input id="file" type="file" name="file"/><br><br>
  <button type="submit">Submit</button>
</form>


如果我理解正确,问题不在客户端实现上,而是在REST API实现上(这意味着当未传递“Content-Length”头时,它不应返回“411”错误HTTP状态代码)。 - rfc1484
2
@rfc1484 我的意思是,在您的fetch()配置中,您应该指定的唯一标头是Authorization标头,因为浏览器会自动设置Content-LengthContent-Type标头。因此,请尝试从代码中删除这两行。 - idbehold
我明白,但我已经尝试过那个解决方案,它没有起作用(返回了之前提到的“411”错误)。由于我正在使用单元测试进行测试,所以我认为问题在于请求是在服务器端执行的,因此在这种情况下,标头可能不会被浏览器自动设置,因为它不通过浏览器并直接使用node-fetch库(在执行服务器端请求时使用的基础库) 。因此,也许这将在客户端起作用,尽管单元测试目前失败。 - rfc1484
教训得到了:始终尝试避免手动设置自己的标头。 ‍♀️ - montrealist

7

我也遇到过类似的问题,具体是在node上使用isomorphic-fetch 发送多部分表单(multipart form)时遇到了问题。对我来说关键是找到了.getHeaders()这个方法。需要注意的是,npm中关于form-data的描述声称不需要这个方法就可以“自动工作”,但至少在node中并不是这样的(我认为浏览器会自动添加头部信息?)。

// image is a Buffer containing a PNG image
// auth is the authorization token

const form_data  = new FormData();
form_data.append("image", png, {
    filename: `image.png`,
    contentType: 'application/octet-stream',
    mimeType: 'application/octet-stream'
});

const headers = Object.assign({
    'Accept': 'application/json',
    'Authorization': auth,
}, form_data.getHeaders());

try {
    const image_res = await fetch(url, {
        method: 'POST',
        headers: headers,
        body: form_data
    });

    if (!image_res.ok) {
        const out = await image_res.json();
        console.dir(out);
        return;
    }
}
catch (e) {
    console.error(`Chart image generation exception: ${e}`);
}

1
感谢Iain。我只有一个小修改-如果您使用@types/form-data,当前版本的@types/form-data(v2.2.1)不支持“mineType”属性,但似乎在没有它的情况下也可以正常工作 :-)另请参阅:https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/form-data/index.d.ts - afrank
9
当我尝试你的代码时,出现了“TypeError:formData.getHeaders不是一个函数”的错误。 - SeriousLee
1
@SeriousLee 它指的是 https://www.npmjs.com/package/form-data 而不是原生的 FormData 对象。 - eomeroff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接