How to Manually Build multipart/form-data Requests in JavaScript and Node.js
This guide explains the different form enctype types, dissects a multipart/form-data example, outlines the encoding rules, and provides step‑by‑step JavaScript and Node.js code for manually constructing and sending such requests, including handling of text fields and binary files.
Common form enctype types
application/x-www-form-urlencoded – default encoding, encodes special characters; used by jQuery AJAX.
multipart/form-data – can submit text and binary data (images, zip, etc.).
text/plain – only encodes spaces; rarely used.
The multipart/form-data type is the most complex because it mixes text fields with binary payloads.
Analyzing a multipart/form-data example
A typical request body consists of a series of parts separated by a boundary string that is declared in the Content-Type header, e.g.:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0yB3cIYoABZUBzEmEach part starts with --boundary, includes a Content-Disposition header that specifies the field name (and optionally a filename), optionally a Content-Type header for binary data, a blank line, the field value, and ends with a CRLF ( \r\n). The final boundary is terminated with --.
Key characteristics of the encoding
The boundary in the body is prefixed with two hyphens ( --) while the header value omits them.
The closing boundary adds two extra hyphens at the end.
Every line ends with \r\n. Between a field’s header line and its value there is an empty line containing only \r\n.
Manual construction in the browser (JavaScript)
When the form contains only text fields, a simple string concatenation with the correct boundaries and headers is sufficient. For mixed text and binary data, the process involves XMLHttpRequest (XHR2), ArrayBuffer, and FileReader APIs.
// Example: build multipart body manually
var boundary_key = 'aotu_lab'; // random separator
var boundary = '--' + boundary_key;
var end_boundary = '
' + boundary + '--';
var result = '';
result += boundary + '
';
result += 'Content-Disposition: form-data; name="user"' + '
';
result += document.querySelector('input[name=user]').value + '
';
result += boundary + '
';
result += 'Content-Disposition: form-data; name="psw"' + '
';
result += document.querySelector('input[name=psw]').value + '
';
result += boundary + '
';
result += 'Content-Disposition: form-data; name="onefile"; filename="pic.png"' + '
';
result += 'Content-Type: image/png' + '
';
// binary data will be appended later
// Convert text part to byte array
var textBytes = [];
for (var i = 0; i < result.length; i++) {
textBytes.push(result.charCodeAt(i));
}
// Convert binary image to Uint8Array (picBuffer is set by FileReader)
var picArray = new Uint8Array(picBuffer);
// Convert end boundary to byte array
var endBytes = [];
for (var i = 0; i < end_boundary.length; i++) {
endBytes.push(end_boundary.charCodeAt(i));
}
var postArray = textBytes.concat(Array.prototype.slice.call(picArray), endBytes);
var postTypedArray = new Uint8Array(postArray);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// handle response
}
};
xhr.open('POST', '/submit');
xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary_key);
xhr.send(postTypedArray.buffer);Note: This implementation does not handle non‑ASCII characters; proper encoding (e.g., encodeURIComponent) would be required for Chinese or other multibyte text.
Node.js version
The server‑side construction follows the same boundary logic but uses the built‑in http module and streams the binary file.
var boundary_key = 'aotu_lab';
var boundary = '--' + boundary_key;
var end_boundary = boundary + '--';
var request = http.request({
host: '127.0.0.1',
port: 3000,
path: '/submit',
method: 'POST'
}, function (res) {
res.on('data', function (buf) {
console.log('response from node:');
console.log(buf.toString());
});
});
request.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary_key);
var body = '';
body += boundary + '
';
body += 'Content-Disposition: form-data; name="user"' + '
';
body += 'aotu.io' + '
';
body += boundary + '
';
body += 'Content-Disposition: form-data; name="psw"' + '
';
body += '123456' + '
';
body += boundary + '
';
body += 'Content-Disposition: form-data; name="onefile"; filename="pic.png"' + '
';
body += 'Content-Type: image/png' + '
';
request.write(body); // writes text part
var picStream = fs.createReadStream('./public/images/o2logo.png');
picStream.pipe(request, { end: false });
picStream.on('end', function () {
request.write('
' + end_boundary);
request.end();
});Run the server after npm install and npm start. Access http://127.0.0.1:3000/ to test the browser version and http://127.0.0.1:3000/formdata for the Node.js version.
Limitations and further reading
The manual approach is inefficient for large files because it converts strings to ordinary arrays before merging with binary data, which can be slower than native streaming APIs. It also lacks proper handling of Unicode characters. Readers interested in a production‑ready solution should explore the native FormData API or libraries such as form-data for Node.js.
Source code is available at:
https://github.com/cos2004/formrequest_app.git
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
