Handling multipart/form-data in Deno

Mayank Choubey
Tech Tonic
Published in
5 min readMar 14, 2021

--

An update of this article has been published here.

Purpose

HTTP requests with content type as multipart/form-data are quite common for form submissions. The default content type for form submission is still x-www-form-urlencoded, however, multipart/form-data is very useful when there is a mix of data, like fields, files, etc.

There are at least two ways to handle multipart/form-data in Deno:

  • Use oak’s multipart middleware: This is perhaps the easiest way and might be suitable for a lot of use cases.
  • Use Deno’s native: Deno’s standard library comes with a multipart body parser that can be used to convert multipart/form-data encoded bodies into easy objects

Deno’s native method could be more performant, but it’s a bit verbose. A lot more code needs to be written to use it natively. As we know, native code is usually faster and more efficient than third-party libraries, therefore native parsing could be suitable for high-performance apps.

In this article, we’ll go over the parsing of multipart/form-data for simple cases like containing only fields. In another article, we’ll build over this base, and understand how to handle file uploads.

Usage

To work with multipart/form-data, import of MultipartReader is required:

import { MultipartReader } from "https://deno.land/std/mime/mod.ts";

As mentioned above, native parsing of multipart/form-data is a bit verbose. There are four steps from start to finish. Let’s go over an overview of the steps:

  • Step 1 - Find boundary: The first step is to find the boundary id from the content-type header.
  • Step 2 - Create a reader: The second step is to create a MultipartReader from the request body and the boundary.
  • Step 3 - Parse the form: The third step is to parse the body and save all the parsed data into a form object.
  • Step 4 - Read values: In the last step, read the values from the form object.

That’s all! It is indeed a bit verbose, but not too much. Now that we’ve seen an overview of the steps, let’s go over them one-by-one in detail.

Step 1: Find boundary

This step is about finding the unique boundary id that comes with all the bodies that are encoded with multipart/form-data. The boundary could be anything like ABCD, random integer, random alphanumeric characters, etc. Usually, the boundary is randomly generated. The boundary is present in the content-type header, and it’s present right after the content-type value.

Content-type: <content-type-value>; boundary=<boundary-id>

Here are some examples:

Content-type: multipart/form-data; boundary=---------------------------293582696224464
Content-type: multipart/form-data; boundary=--ABCD
Content-type: multipart/formdata; boundary="----arbitrary boundary"

The actual value of the boundary is of interest to the multipart reader, not for the user. The job of the user is to find the boundary id and give it to the multipart reader. The following code could be used to get the boundary from the header:

const ct = req.headers.get("content-type");
let boundary = "";
if(ct?.startsWith('multipart/form-data'))
boundary=ct?.split(";")[1]?.split("=")[1];

The code is simple. First, split on the semicolon, and then split on =.

Step 2 Create Reader

This step is for creating a MultipartReader that takes two inputs:

  • request body
  • boundary (from step 1)
const mr = new MultipartReader(req.body, boundary);

It should be noted that the creation of MultipartReader isn’t the same as parsing of the body. The actual parsing of the body happens in step 3.

Step 3 Read the form

This is the most computation-intensive step as this is where the parsing is done. The good part is that the parsing function is async.

const form = await mr.readForm();

There is an optional input to the readForm function: maxMemory. The default value of maxMemory is 10MB. If the incoming form data exceeds 10MB, Deno would store the overflow in temporary files. This memory can be increased at the time of reading the form:

const form = await mr.readForm(20 << 20);
//Equivalent of 20971520 bytes

This step would take time as bodies encoded with multipart/form-data could be big. Once the parsing is complete, a form object would be returned.

Step 4 Read values

This is the last step. In this step, values can be read from the form object. The form object provides two functions to read the data:

  • value: Returns the value for a given key. This function is useful in getting values of fields where keys are known.
curl -F name=abcd -F age=40 http://localhost:3000form.value('name');
//abcd
form.value('age');
//40
  • entries: Returns an iterator to go over all the keys and values. This function is useful in reading all the values one-by-one. In every iteration, it returns an array containing the data.
curl -F name=abcd -F age=40 http://localhost:3000for(const entry of form.entries())
entry; //do something with entry
//[ "name", "abcd" ]
//[ "age", "40" ]

That’s all about natively reading simple multipart/form-data in Deno. As mentioned earlier, there is a bit of work involved, but not too much.

Now that we’ve all the background, let’s go over some more examples.

Examples

The first example is about reading the form fields using the value function. Here is the curl command:

curl -F A=B -F C=D -F G=H -F Z=1 -F Y=2 http://localhost:3000

Here is the complete code to get some of the fields:

import { serve } from "https://deno.land/std/http/server.ts";
import { MultipartReader } from "https://deno.land/std/mime/mod.ts";
const s = serve({ port: 3000 });
for await (const req of s) {
const ct = req.headers.get("content-type");
let boundary=ct?.split(";")[1]?.split("=")[1];
const mr = new MultipartReader(req.body, boundary);
const form = await mr.readForm();
req.respond({ body: JSON.stringify({A: form.value('A'), Z: form.value('Z')}) });
}
//{"A":"B","Z":"1"}

Only fields A and Z are read from the form, though the form has more fields. It’s important to note that all the data is considered as a string, so it’d need conversion to numbers, date, etc if required.

The second example is about reading the form fields using the entries function. This could be useful for cases when either the fields need to be iterated one-by-one, or the fields/keys are not known.

curl -F A=B -F C=D -F G=H -F Z=1 -F Y=2 http://localhost:3000import { serve } from "https://deno.land/std/http/server.ts";
import { MultipartReader } from "https://deno.land/std/mime/mod.ts";
const s = serve({ port: 3000 });
for await (const req of s) {
const ct = req.headers.get("content-type");
if(ct?.startsWith('multipart/form-data')) {
const boundary=ct?.split(";")[1]?.split("=")[1];
const mr = new MultipartReader(req.body, boundary);
const form = await mr.readForm();
const retBody:any={};
for(const entry of form.entries())
retBody[entry[0]]=entry[1];
req.respond({ body: JSON.stringify(retBody) });
}
}
//{"A":"B","C":"D","G":"H","Z":"1","Y":"2"}

--

--