Beware implicit Content-Type: "application/json"
Published on in JavaScript, Node.js and TypeScript
If you are sending a FormData
body with fetch
,
things might not work correctly if you are (accidentally)
setting the Content-Type
request header to "application/json"
.
Table of contents
Problem
I was sending a FormData
object to a 3rd party API:
const formData = new FormData()
formData.append('foo', someValue)
const response = await request('https://example.com/api/', {
body: formData,
method: 'POST',
})
The API responses were confusing/incorrect.
Culprit
I was using request
,
which is a thin wrapper around fetch
.
Something like this:
async function request(url: string, options?: RequestInit) {
const response = await fetch(url, {
...options,
headers: {
Accept: 'application/json',
apiKey: 'abc123',
'Content-Type': 'application/json',
...options.headers,
},
})
if (response.ok) {
return response.json()
}
// Handle errors etc.
}
Notice how I'm defaulting the Content-Type
header to "application/json"
.
This is handy because most of the time I'm sending JSON.
But now I was sending a FormData
object,
so the implicit "application/json"
value caused problems.
It took me a while to realize that this was the culprit for the confusing API responses.
Fix attempt 1
First I tried to "clear" the default Content-Type
value:
const response = await request('https://example.com/api/', {
body: formData,
headers: {
'Content-Type': undefined,
},
method: 'POST',
})
But whoops,
it caused a TypeScript error (header values must be strings),
and the value was set to the string "undefined"
.
Not good.
Fix attempt 2
Then I tried to set the value to "multipart/form-data"
:
const response = await request('https://example.com/api/', {
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
method: 'POST',
})
But this didn't work either.
Explanation
This snippet makes it clear why the 2nd fix attempt didn't work:
const formData = new FormData()
const req = new Request('https://example.com/api/', {
body: formData,
method: 'POST',
})
console.log(...req.headers)
//=> ['content-type', 'multipart/form-data; boundary=---------------------------26864524337159351664014536661']
So my Content-Type
header value ("multipart/form-data"
)
was missing a boundary
value,
which is automatically generated by the browser (or Node.js or whatever)
and actually required.
From Content-Type
on MDN:
For multipart entities, the
boundary
parameter is required. It is used to demarcate the boundaries of the multiple parts of the message. [...]
[A
multipart/form-data
] request looks something like the following example with some headers omitted for brevity. In the request, a boundary ofExampleBoundaryString
is used for illustration, but in practice, a browser would create a string more like this---------------------------1003363413119651595289485765
.POST /foo HTTP/1.1 Content-Length: 68137 Content-Type: multipart/form-data; boundary=ExampleBoundaryString --ExampleBoundaryString Content-Disposition: form-data; name="description" Description input value --ExampleBoundaryString Content-Disposition: form-data; name="myFile"; filename="foo.txt" Content-Type: text/plain [content of the file foo.txt chosen by the user] --ExampleBoundaryString--
Final fix
The implicit Content-Type: "application/json"
was the root cause,
so I got rid of it.
Well, at least partially:
async function request(url: string, options?: RequestInit) {
const response = await fetch(url, {
...options,
headers: {
Accept: 'application/json',
apiKey: 'abc123',
...(!(options.body instanceof FormData) && {
'Content-Type': 'application/json',
}),
...options.headers,
},
})
// ...
}
Now if the body
is a FormData
object,
fetch
automatically sets the Content-Type
header to multipart/form-data
with an automatically generated boundary
value.
Lessons
My first thought was that while this hand-rolled wrapper around fetch
has been handy,
maybe it would have been better to use a battle-tested library from the beginning.
For example,
Ky ("a tiny and elegant HTTP client based on the Fetch API")
seems nice.
Or, at least, one should be careful about default/implicit values.
I wonder if the 3rd party API server should also be validating the request headers.
The API endpoint expects a FormData
body,
so maybe it should also check that the Content-Type
request header is valid.
It would be a small thing to do,
but it would have saved me a great amount of time.