I post FormData
(including files) to a server that rejects requests with a content-length that exceeds a specific limit. I'd like to validate the content-length in my JavaScript client (browser) before doing a request that is doomed to be rejected. How do I get the content-length of my (multipart/form-data
) encoded FormData
object?
const formData = new FormData();
formData.append('text', text);
formData.append('file', file);
if (getContentLength(formData) > limit) {
alert('Content length limit is exceeded');
} else {
fetch(url, { method: 'POST', body: formData });
}
Edit: Thanks for your answer, @Indgalante. Just using length
of a string and size
of a file does not calculate the correct content length.
function getContentLength(formData) {
const formDataEntries = [...formData.entries()]
const contentLength = formDataEntries.reduce((acc, [key, value]) => {
if (typeof value === 'string') return acc + value.length
if (typeof value === 'object') return acc + value.size
return acc
}, 0)
return contentLength
}
const formData = new FormData();
formData.append('text', 'foo');
alert(`calculated content-length is ${getContentLength(formData)}`);
fetch('', { method: 'POST', body: formData });
I post FormData
(including files) to a server that rejects requests with a content-length that exceeds a specific limit. I'd like to validate the content-length in my JavaScript client (browser) before doing a request that is doomed to be rejected. How do I get the content-length of my (multipart/form-data
) encoded FormData
object?
const formData = new FormData();
formData.append('text', text);
formData.append('file', file);
if (getContentLength(formData) > limit) {
alert('Content length limit is exceeded');
} else {
fetch(url, { method: 'POST', body: formData });
}
Edit: Thanks for your answer, @Indgalante. Just using length
of a string and size
of a file does not calculate the correct content length.
function getContentLength(formData) {
const formDataEntries = [...formData.entries()]
const contentLength = formDataEntries.reduce((acc, [key, value]) => {
if (typeof value === 'string') return acc + value.length
if (typeof value === 'object') return acc + value.size
return acc
}, 0)
return contentLength
}
const formData = new FormData();
formData.append('text', 'foo');
alert(`calculated content-length is ${getContentLength(formData)}`);
fetch('https://httpbin/post', { method: 'POST', body: formData });
You did not consider that the form data is encoded in the request. Thereby, i.a. boundary is added. The calculated content-length in the example is 3, but should be 138 in my Chrome browser
and 172 in my Firefox browser. I'm unsure how other browsers behave.
Share Improve this question edited Jun 9, 2020 at 14:30 maiermic asked Jun 9, 2020 at 11:39 maiermicmaiermic 4,9849 gold badges42 silver badges82 bronze badges 2- Have you solved this? I face a related question in Safari. – daxvwv Commented Aug 18, 2020 at 10:57
- @Soyaine Not really. I use an estimation as described in my answer stackoverflow./a/63471719/1065654 – maiermic Commented Aug 18, 2020 at 15:21
3 Answers
Reset to default 2I still don't know if it is possible to calculate the exact size, but you can at least try to estimate it:
/**
* Estimate the content length of (multipart/form-data) encoded form data
* object (sent in HTTP POST requests).
* We do not know if you can get the actual content length.
* Hence, it is estimated by this function.
* As soon as {@link https://stackoverflow./q/62281752/1065654 this}
* question is answered (correctly), the correct calculation should be used.
*
* @param formData
*/
function estimateContentLength(formData: FormData) {
// Seems to be 44 in WebKit browsers (e.g. Chrome, Safari, etc.),
// but varies at least in Firefox.
const baseLength = 50; // estimated max value
// Seems to be 87 in WebKit browsers (e.g. Chrome, Safari, etc.),
// but varies at least in Firefox.
const separatorLength = 115; // estimated max value
let length = baseLength;
const entries = formData.entries();
for (const [key, value] of entries) {
length += key.length + separatorLength;
if (typeof value === 'object') {
length += value.size;
} else {
length += String(value).length;
}
}
return length;
}
Here is a async version that first converts the FormData into a Blob. from which you can retrieve the actual size that is going to be sent to the server.
So instead of posting the formdata you send the generated blob instead.
async function test() {
// Create some dummy data
const fd = new FormData()
fd.set('a', 'b')
// acquire an actual raw bytes as blob of what the request would send
const res = new Response(fd)
const blob = await res.blob()
blob.text && (
console.log('what the actual body looks like'),
console.log(await blob.text())
)
// can't use own blob's type since spec lowercase the blob.type
// so we get the actual content type
const type = res.headers.get('content-type')
// the acutal content-length size that's going to be used
console.log('content-length before sending', blob.size)
// verify
const testRes = await fetch('https://httpbin/post', {
method: 'POST',
body: blob, // now send the blob instead of formdata
headers: { // use the real type (and not the default lowercased blob type)
'content-type': type
}
})
const json = await testRes.json()
const headers = new Headers(json.headers)
console.log('form that got posted:', JSON.stringify(json.form))
console.log('content-length that was sent', headers.get('content-length'))
}
test()
However this don't work in IE, and Safari
- IE don't have fetch (but it's dead anyway)
- Safari has this bugs.
- https://bugs.webkit/show_bug.cgi?id=161190
- https://bugs.webkit/show_bug.cgi?id=212858
- https://bugs.webkit/show_bug.cgi?id=171581
all doe, replacing the formdata with a polyfilled version such as https://github./jimmywarting/FormData might assist you with converting a formdata into a blob directly (synchronous) without using the fetch api (using formData._blob()
). that is if you need wider browser support
One approach could be FormData.entries()
so you can iterate over all your data and get a final content length.
We will also need a spread operator
over an array or you could use Array.from()
to convert the iterator returned by .entries()
into a proper array.
Code example below, I didn't test it with files but let me know if you have any edge case with this one.
function getContentLength(formData) {
const formDataEntries = [...formData.entries()]
const contentLength = formDataEntries.reduce((acc, [key, value]) => {
if (typeof value === 'string') return acc + value.length
if (typeof value === 'object') return acc + value.size
return acc
}, 0)
return contentLength
}