Understanding Form Data in HTML
Introduction
Forms (form) are a very common feature in web applications, capable not only of transmitting plain text but also enabling file upload functionality. However, because form behavior differs somewhat from other transmission methods, it can sometimes cause confusion and misunderstandings.
By examining the specifications to trace the full context, this article takes a deep dive into what goes on behind the scenes of forms, how Form Data differs from other data transfer methods, and finally, what the HTML <form/> tag actually does under the hood.
It mainly covers the following key points:
- What
multipart/form-datais and why we need it - How to understand the request format
- Understanding what problems form-data solves
Why Do We Need Form Data?
Data transmission requires both sides to share an understanding of the data format. In the world of networking, we use protocols to standardize how data is transmitted. Through HTTP’s Content-Type header, we can determine the nature of a request’s content and interpret the data accordingly.
MIME Types define the categories of transfer formats:
Content-Type: application/jsonindicates that the request body is JSONContent-Type: image/pngindicates that the request body is an image file
Among these, multipart/form-data is just one type of Content-Type.
A typical Content-Type usually only transmits a single format of data. In web applications, however, we may want to upload files, images, and videos along with form fields. This requirement led to the creation of the multipart/form-data specification (RFC 7578).
Anatomy of a Form Data Request
The greatest advantage of multipart/form-data is that it allows users to send multiple data formats at once in a single request. It is primarily used in HTML forms or when implementing file upload functionality.
Next, let’s observe what a multipart/form-data payload looks like. To send a request with a Content-Type of multipart/form-data, you can use the HTML form tag (or JavaScript’s FormData):
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="text" name="name" />
<input type="file" name="file" />
<button>Submit</button>
</form>
When you click the Submit button, the browser sends a POST request:
POST /upload HTTP/1.1
Host: localhost:3000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFYGn56LlBDLnAkfd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"
Test
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
Hello World
WebKitFormBoundaryFYGn56LlBDLnAkfd--
Because web requests are built on HTTP, multipart/form-data is also a standard HTTP request whose format is defined in RFCs.
Understanding a multipart/form-data request comes down to two key points:
- Understanding the role of the boundary
- Understanding the meaning of each format section
The Role of Boundary
Content-Type: multipart/form-data; boundary=——WebKitFormBoundaryFYGn56LlBDLnAkfd
Inside the Content-Type header, we can see a strange string following boundary=. What is the purpose of this boundary?
As mentioned earlier, the goal of multipart/form-data is to allow data of different formats to be sent through the same request. Therefore, there must be a way to tell where each piece of data begins and ends. Taking query parameters as an example, in a=b&c=d, the & serves as a delimiter so the computer knows when to split the data. Whenever the computer encounters this boundary string, it knows that reading for the current attribute’s data is complete, and it can start reading the next piece of data.

The specification does not strictly enforce the exact format of the boundary, but it does define length limits and permitted characters:
- Starts with two hyphens
- Total length within 70 characters (excluding the hyphens themselves)
- Only accepts ASCII 7-bit
Therefore, a string like helloworldboundary is also a completely valid boundary.
Content-Disposition
In multipart/form-data, the purpose of Content-Disposition is to describe the format of that part’s data.
Content-Disposition: form-data; name="name"
This specifies that this is a field inside Form Data named name.
If it is a file, filename will also be appended, and a Content-Type header will be added on the next line to describe the file type:
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
After an empty line, the actual data content follows:
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"
Test
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
Hello World
WebKitFormBoundaryFYGn56LlBDLnAkfd--
In the example above, I uploaded a plain text file. If an image file or another file format is used, it will be represented in binary form:
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
PNG
IHDR¤@¬
ÃiCCPICC ProfileHTSÙϽétBoô*%ôÐ{³@B!!ØPGp,¨2 cd,(¶A±a :l¨¼<ÂÌ{ë½·Þ¿ÖY÷»;ûì½ÏYçܵÏ
(omitted)
Implementing a multipart/form-data Request
Now that we understand the request format of multipart/form-data, we can write one ourselves to see it in action. Here is an example using Node.js:
const http = require('http');
const fs = require('fs');
const content = fs.readFileSync('./text.txt');
const formData = {
name: 'Kalan',
file: content,
};
let payload = '';
const boundary = 'helloworld';
Object.keys(formData).forEach((k) => {
let content;
if (k === 'file') {
content = [
`\r\n--${boundary}`,
`\r\nContent-Disposition: multipart/form-data; name=${k}; filename="text.txt"`,
`\r\nContent-Type: text/plain`,
`\r\n`,
`\r\n${formData[k]}`,
].join('');
} else {
content = [
`\r\n--${boundary}`,
`\r\nContent-Disposition: multipart/form-data; name=${k}`,
`\r\n`,
`\r\n${formData[k]}`,
].join('');
}
payload += content;
});
payload += `\r\n--${boundary}--`;
const options = {
host: 'localhost',
port: '3000',
path: '/upload',
protocol: 'http:',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=helloworld',
'Content-Length': Buffer.byteLength(payload),
},
};
const req = http.request(options, (res) => {});
req.write(payload);
req.end();
The implementation is very simple: it merely constructs the request body according to the format defined in the specification. The main thing to keep in mind is that every boundary starts with two hyphens, and the final boundary ends with an additional two hyphens.
Next, we can use Wireshark to observe whether the packet contents are parsed correctly:


In the “Encapsulated multipart part” section, we can see that both name=Kalan and the file content were parsed correctly. This demonstrates several things:
multipart/form-datais simply a type of HTTP request- As long as it adheres to the format, requests can be sent without a browser
- File content must be parsed on the server side (the request just transmits a chunk of binary data)
I find that many beginners overlook this last point: sending a request via multipart/form-data doesn’t mean the backend magically receives an already-extracted file object. It must be parsed before the file content becomes accessible in a format that is convenient to work with. For instance, in Node.js, a popular package for handling file uploads is multer, which parses the incoming file content.
application/x-www-form-urlencoded
If you submit a form using the GET method, all form content will be sent URL-encoded. For example, clicking Submit on the HTML below will navigate to /upload?name=Kalan&file=filename. Even if enctype is specified as multipart/form-data, it will still be sent as application/x-www-form-urlencoded:
<form enctype="multipart/form-data" action="/upload" method="GET">
<input type="text" name="name" />
<input type="file" name="file" />
<button>Submit</button>
</form>
Conclusion
This article explored multipart/form-data from the perspective of its specifications, discussed what problems Form Data solves, and walked through manually building a compliant multipart/form-data request to gain a deeper understanding of this uniquely structured HTTP request.
multipart/form-data offers several benefits for web applications:
- Data in different formats can be sent within a single request
- It fulfills users’ need to upload files
- Browsers have a standardized specification to implement
For developers, understanding multipart/form-data serves several purposes:
- Knowing the underlying principles of file uploads on the web
- Understanding how HTTP requests standardize data transmission across different formats
- Accelerating development by mastering the fundamentals
The next article focuses on the <form> tag itself, diving into how browsers handle this HTML tag and what we as developers should watch out for.
Related Posts
- Recreating My Room with Three.js Using React Three Fiber, I brought my real room into the browser—turning physical objects into an interactive table of contents, and using spatial memory to tell the story of my life and work over the past few years.
- Things to Keep in Mind When Using Images in Frontend Development Expanding on Jake Archibald's article, this post organizes how modern responsive images should be written: why width/height are still necessary, when to use CSS aspect-ratio, how to choose between AVIF and WebP, and using picture/source/srcset for art direction on mobile devices.
- CSS field-sizing — Auto-resize Form Elements with a Single Line of CSS Previously, auto-resizing a textarea required listening to scrollHeight in JavaScript. With CSS field-sizing: content, a single line replaces it all, supporting textarea, input, and select. This article covers the pain points of older approaches and how to use field-sizing.
- Make Your Link Underlines Look Better: text-underline-offset By default, underlines sit very close to the text. Some designers dislike this look, and personally, I don't think it looks great either.