A few gotchas with the interaction between the FormData
and fetch
APIs, for those used to $.ajax
and xhr
.
The browser wraps any Fetch or XHR request sending FormData
In a recent project, we needed to upload images to Rackspace Cloud Files from a form. After a bit of googling, we quickly arrived at code reminiscent of the following:
export function* uploadImage({ data, params }) {
// request a signed Rackspace Cloud Files url and object path to upload to, from our backend
const urlResp = yield call(client.postUploadUrl, null, params)
const { url, objectPath } = urlResp
// wrap data to upload in a FormData object - turns out this is not needed/wanted
const formData = new FormData(data)
// setup necessary HTTP headers
const headers = {
"Access-Control-Expose-Headers": "Access-Control-Allow-Origin",
"Access-Control-Allow-Origin": "*",
"Content-Disposition": "attachment",
"Content-Type": "image/png",
}
// upload the logo to the returned url
yield call(fetch, url, { body: formData, method: "PUT", headers })
}
To our surprise, this caused a parsing error on Rackspace. Mysteriously, our image data was arriving at the server wrapped with ------WebKitFormBoundaryJ0uWMNv29fcUxC1t--
, and Rackspace didn't like that one bit.
According to StackOverflow, the conventional wisdom was to write some server-side code to parse out the WebKitFormBoundary
nonsense, but unfortunately we don't control RackSpace and thus can't control how how our uploads get parsed.
After reimplementing the above using raw xhr
resulted in the same behavior, we tried removing FormData
from the equation. Lo and behold, the WebKitFormBoundary
was gone and our images were happily slurped up by Rackspace.
TL;DR: Don't wrap binary requests in FormData
.
Fetch respects the headers you set, XHR knows better
In the same project, we also had a use case for uploading CSV files. This time we were not interfacing with Rackspace, but rather our own API backend using gocsv for CSV processing.
Nonetheless, we once again found ourselves using a combination of fetch
and FormData
.
export function* importCsv({ file }) {
const formData = new FormData()
formData.append('upload', file)
const headers = {
...
'Content-Type': 'multipart/form-data'
}
yield call(fetch, url, { body: formData, method: 'POST', headers })
}
Again, seemingly innocuous code was somehow failing. Our API returned 400 errors, claiming it couldn't find a 'form boundary'. But how does one add a form boundary? This time using xhr
instead of fetch
fixed the issue -- it was adding the form boundaries for us. But why? As it turns out xhr
was blowing away our Content-Type
header and appending its own, containing the appropriate form boundary, while fetch
was respecting the Content-Type
header we set and allowing it to fail. So when using fetch
to submit multipart
forms, the way to properly set the Content-Type
header is to just leave it off.
TL;DR: Don't set the Content-Type
header and the browser will do it for you.