· 8 min read

Understanding Form Data in HTML

This article was auto-translated from Chinese. Some nuances may be lost in translation.

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-data is 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/json indicates that the request body is JSON
  • Content-Type: image/png indicates 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.

Group 2

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:

form-data HTTP request packet analysis

form-data HTTP request packet analysis 2

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-data is 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

Explore Other Topics