Parsing post data 3 different ways in Node.js without third-party libraries
By: Salvador Guerrero
Tags: Coding, Nodejs, JavaScript, Programming, Json

3. multipart/form-data

This one was the trickiest one of all, this is not close to being a one liner; if it’s so complicated why bother? The simple answer is transferring files with form data in a single request.

There are different approached how to parse this data, but for the purpose of this example I’m loading all data to memory, then storing the given file to the server and then of course as the previous examples also returning the request data as response data, I remove the binaries from the response because I’m showing the data on the website and loading binary takes a long time. The HTML form is simpler than application/json , it’s basically the same as application/x-www-form-urlencoded but with different encoding (enctype)

<form method="post" enctype="multipart/form-data">

<input id="username3" type="text" name="username">

<input id="password3" type="password" name="password">

<input id="picture3" type="file" name="picture">

<input type="submit">

</form>

What’s different from x-www-form-urlencoded is that we not explicitly assign enctype to multipart/form-data and add a file input type named picture to the mix.

I use the same data and end emit event callbacks as the previous methods, but this time, before we register the callbacks we change the request encoding to latin1 so that it reads the binary data correctly.

if (request.headers['content-type'] === 'multipart/form-data') {

// Use latin1 encoding to parse binary files correctly

request.setEncoding('latin1')

}

The data sent by multipart/form-data is separated in parts divided by a boundary specified in the content-type header sent by the browser, an example below:

------WebKitFormBoundaryxJi9AgGdxx83BunR

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

------WebKitFormBoundaryxJi9AgGdxx83BunR

Content-Disposition: form-data; name="password" pass

------WebKitFormBoundaryxJi9AgGdxx83BunR

Content-Disposition: form-data; name="picture"; filename="Chromium_11_Logo.svg"

Content-Type: image/svg+xml

<!-- Created with Inkscape ( <svg>

...

</svg> ------WebKitFormBoundaryxJi9AgGdxx83BunR--

The boundary in the above example is ------WebKitFormBoundaryxJi9AgGdxx83BunR , I created a helper function to extract the boundary from the request header as follows:

function getBoundary(request) {

let contentType = request.headers['content-type']

const contentTypeArray = contentType.split(';').map(item => item.trim())

const boundaryPrefix = 'boundary='

let boundary = contentTypeArray.find(item => item.startsWith(boundaryPrefix))

if (!boundary) return null

boundary = boundary.slice(boundaryPrefix.length)

if (boundary) boundary = boundary.trim()

return boundary

}

In the above method, I read the information from the content-type header, then I split it by ; , then I trim whitespaces from each entry in the content-type, then I get the boundary by running the find() method on the array by checking the boundary= as prefix, I then return the string after this prefix.

Now that we know how to get the boundary, we’re going to use it to split the request post body as follows:

const boundary = getBoundary(request)

let result = {}

const rawDataArray = rawData.split(boundary)

for (let item of rawDataArray) {

...

}

Within the for loop the first thing I check is for the name:

// Use non-matching groups to exclude part of the result

let name = getMatching(item, /(?:name=")(.+?)(?:")/)

if (!name || !(name = name.trim())) continue

I use non-matching groups regex search to exclude part of the matching string so that it returns just the data I’m looking to get, which is just the name matched with (.+?) , if no name was found continue to the next item.

Next up I get the value for the given named input with a kinda complicated regex search, also using non-matching groups:

let value = getMatching(item, /(?:\r

\r

)([\S\s]*)(?:\r

--$)/)

if (!value) continue

The above matches all characters including spaces, newlines and tabs, the value is matched with ([\S\s]*) , and if no value was found we continue to the next item in the array.

Next up is to check if this entry is a file, and we find that out if we find a filename as follows:

let filename = getMatching(item, /(?:filename=")(.*?)(?:")/)

if (filename && (filename = filename.trim())) {

...

}

Filename can be empty, If the user didn’t select any file and presses the submit button, that’s why I match the filename using (.*?) to match 0 or more characters.

If the filename if valid I also search for content-type as follows:

let contentType = getMatching(item, /(?:Content-Type:)(.*?)(?:\r

)/)

the above regex string matches the content type with (.*?) .

Because there can be several files in a post request in the example I store all the files in a resulting array and to store the files in the server I do the following:

for (let file of data.files) {

const stream = fs.createWriteStream(file.filename)

stream.write(file.picture, 'binary')

stream.close()

file.picture = 'bin'

}