14

This is a CURL example which works fine:

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

I'm trying to reproduce this request using isomorphic-fetch.

I've tried the following code:

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
})`

I use fs.readFileSync in order to generate the file passed to FormData.

The previous example returns a 401 HTTP status code (unauthorized) with an error message saying that the userId embedded in the token (sent via header) does not match the userId passed from formData.

So my suspicion is that the FormData that arrives to the REST API is not adequately formed.

The problem may be related with the Content-Length header, but I didn't find a better way to calculate it (if I don't use the Content-Length header I get a 411 HTTP status code Content-Length header missing).

Could be the case that this is failing because of an incorrect value in the Content-Length header?

Any other suggestions on why this is failing or how to better debug it?

If further info is needed to clarify this problem, please just ask.

UPDATE

I've tried the form-data module in order to get the right Content-Length value using the method formData.getLengthSync()

However the problem remains the same (401 error HTTP status code response).

rfc1484
  • 9,441
  • 16
  • 72
  • 123
  • 2
    Drop the `Content-Type` request header as that needs to be automatically generated by the browser to include the multipart boundary. I think if you drop that and the `Content-Length` headers you should be okay. – idbehold Apr 19 '17 at 19:08
  • I've already tried that without success, when I don't send the `Content-Length` header the `API` returns a `411` error HTTP status code: `The server refuses to accept the request without a defined Content-Length` – rfc1484 Apr 19 '17 at 21:06
  • Try setting the Content-Length to 12345. Whatever server you're uploading to wasn't designed very well. – idbehold Apr 19 '17 at 21:21
  • Did not work either, I don't really understand why the `Content-Length` header seems to be necessary when I perform a `fetch` request but when I perform the same request via `CURL` without this header, all works fine and a `411` error HTTP status code is not returned. – rfc1484 Apr 20 '17 at 07:28
  • Not sure if you ever figured this out, but I believe your problem was that you had "Content-Type", when you should have had "Content-Disposition". The "Content-Length" is not needed. – Eric J. Crammer Jul 15 '19 at 19:02
  • The boundary string is required if you set the content-type manually! [see this post](https://stackoverflow.com/q/3508338/2803565). It is easier to don't specify those headers for formData type – S.Serpooshan Aug 11 '22 at 11:33

2 Answers2

12

Just remove the Content-Length and Content-Type headers from your code as these headers will be set automatically by the browser.

If you open up your network inspector, run this code snippet, and submit the form you should see that the Content-Length is set correctly:

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>

S.Serpooshan
  • 7,608
  • 4
  • 33
  • 61
idbehold
  • 16,833
  • 5
  • 47
  • 74
  • So if I understand correctly the problem is not with the client implementation but with the REST API implementation (meaning it shouldn't return a `411` error HTTP status code when the `Content-Length` header is not being passed) – rfc1484 Apr 20 '17 at 15:51
  • 2
    @rfc1484 I'm saying that the only header you should be specifying in your `fetch()` configuration is the `Authorization` header because both the `Content-Length` and `Content-Type` headers will be set automatically by the browser. So please try just removing those two lines from your code. – idbehold Apr 20 '17 at 16:10
  • I understand, but I already tried that solution and it didn't work (it returned the `411` error mentioned before). Since I'm testing this using a unit test, I think the problem is that the request is performed server-side, so may be in that case the headers won't be set automatically by the browser, since it does not go through a browser and uses the `node-fetch` library directly (the underlying library used by `isomorphic-fetch` when performing server-side requests). So may be this will work `client-side` although the unit test is currently failing. – rfc1484 Apr 20 '17 at 16:37
  • Lesson learned: always try to avoid setting my own headers manually. ‍♀️ – montrealist Oct 25 '19 at 20:02
7

I hit my head against a similar wall, specifically using isomorphic-fetch on node to POST a multipart form. The key for me was finding .getHeaders(). Note that NPM description for form-data suggests that it'll "just work" without this, but it doesn't seem to, at least not in node (I think browsers inject header stuff?).

// 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}`);
}
Iain Bryson
  • 390
  • 5
  • 11
  • 1
    Thanks Iain. I have just a minor modification -- if you use it with @types/form-data, the current version of @types/form-data (v2.2.1) does not support the "mineType" attribute, but it seems to work fine without it :-) See also: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/form-data/index.d.ts – afrank Apr 20 '18 at 03:01
  • 9
    I get `TypeError: formData.getHeaders is not a function` when I try your code. – SeriousLee Nov 03 '19 at 19:10
  • 1
    @SeriousLee it is referring to https://www.npmjs.com/package/form-data and not native FormData object – eomeroff Jul 19 '21 at 16:24