How FormData Handles Multipart Uploads in JavaScript

The JavaScript FormData interface provides a streamlined way to construct a set of key-value pairs representing form fields and their values, which can be easily transmitted using HTTP requests. This article explains how FormData captures data, structures text and binary files, automatically generates multipart boundaries, and prepares the payload for multipart/form-data encoding without requiring manual string concatenation or header management.

Instantiating and Populating Data

A FormData object can be instantiated empty or populated directly from an existing HTML <form> element:

// Empty instance
const formData = new FormData();

// From an HTML form element
const formElement = document.querySelector('form');
const populatedData = new FormData(formElement);

You populate data using two primary methods: * append(name, value, [filename]): Adds a new value to an existing key or creates the key if it does not exist. * set(name, value, [filename]): Overwrites any existing value for the given key with the new value.

Handling Text vs. Binary Payloads

When appending values, FormData differentiates between text strings and binary objects (Blob or File):

  1. Text Fields: If the value is not a Blob or File, the browser converts it to a string.
  2. Binary Files: When passing a Blob or File, an optional third argument specifies the filename. If omitted for a Blob, it defaults to "blob".
formData.append('username', 'alex_dev');
formData.append('profilePicture', fileInput.files[0], 'avatar.png');

Constructing the Multipart Payload Structure

Under the hood, FormData serializes entries into the multipart/form-data format (defined by RFC 7578). The payload is composed of multiple distinct parts separated by a unique delimiter called a boundary.

For each key-value pair, the browser generates: * Boundary Delimiter: A unique string guaranteed not to occur within the data payloads. * Content-Disposition Header: Identifies the field name and, for files, the filename. * Content-Type Header: Added automatically for binary data based on the Blob.type or file extension (defaults to application/octet-stream if unknown). Plain text fields typically omit this or default to text/plain. * Payload Body: The raw text or binary stream followed by a carriage return and line feed (\r\n).

An assembled payload internally resembles the following structure:

--WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

alex_dev
--WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="profilePicture"; filename="avatar.png"
Content-Type: image/png

<binary data>
--WebKitFormBoundary7MA4YWxkTrZu0gW--

Transmission with Fetch and XMLHttpRequest

When passing a FormData instance as the body of a fetch request or XMLHttpRequest, the browser automatically computes the correct Content-Type header along with the generated boundary parameter:

fetch('/api/upload', {
  method: 'POST',
  body: formData
});

The resulting request header looks like: Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWxkTrZu0gW

Manually setting the Content-Type header when sending FormData must be avoided, as doing so removes the boundary parameter and prevents the server from correctly parsing the individual key-value pairs.