Blog   Home

FormData Fetch Gotchas

javascript · formdata · fetch · gotchas

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.