Serge van den Oever's weblog

Processing multipart/form-data in NodeJS Azure function with HttpTrigger

September 25, 2020 • ☕️ 5 min read

Introduction

Microsoft’s answer to the JAM-stack hosting platforms like Netlify and Cloudflare Workers is Azure Static Web Apps.

All platforms provide similar ingredients, but they have big differences in the details. First about the similar ingredients:

  • Hosting of static content, made available globally through a CDN
  • Compute to enable you to build HTTP endpoints like API’s and webhooks - written with NodeJS or other languages
  • Support for CI/CD
  • Management for domain configuration, authentication/authorization, environment settings and much more

One of those big differences in the details I stumbled upon was in handling the posting of a form with multipart/form-data to support both form fields and files. In my case I needed to handle a set of fields and a set of images.

The long-winded road to nirvana

I started out with Netlify as the platform of choice for my solution because it is simple and free. After a lot of trials and (always) errors I stumbled upon a discussion thread that made clear that the (crippled) implementation on Netlify functions does not support posting of binary data. The underlying AWS Lambdas do have support, but this support is not enabled for Netlify. Another hugely annoying thing of Netlify functions is that there is no information on how to debug Netlify functions with VSCode. You only see ramblings like this discussion thread on the topic, and some pointer in the right direction in this discussion thread. This should be part of the documentation! Debugging using console.log() is so… 90’s.

My next step was Azure Static Web Apps, which has the backing for compute through the really mature Azure Functions. First of all: VSCode has great support for creating, managing and debugging Azure Static Web Apps and the underlying functions through the Azure Static Web Apps extension for Visual Studio Code.

But most important: it can handle POST messages with binary data!

Because parsing multipart/form-data is a tricky piece of work I tried a lot of npm package of which others reported success with. An often used npm package is parse-multipart as described in the blog post Multipart/form-data processing via HttpTrigger using NodeJS Azure functions and this discussion thread. I did only get it working when I only had file fields, but with a mixture of text and file fields it completely crashed on me. After investigating other packages like multiparty and multer and many others that didn’t work either, I even tried the code of Parsing post data 3 different ways in Node.js without third-party libraries but could not get it working.

nirvana

To cut a long story short, I finally got it working. I stumbled upon a low-level package busboy (what’s in a name!) that describes itself as A node.js module for parsing incoming HTML form data. From the documentation I could not get it working because it only describes how to hook into the streaming model of getting data, that is applicable in an Express or http package based situation using req.pipe(busboy). In an Azure function the request body is already completely loaded.

After digging through the source code of the package I found some tests that used exactly the approach that I needed by using the undocumented call busboy.write(request.body, function() {}); where I can pass-in the body data that we already have available in the Azure Function. This brought me to the following code. I log information about all the fields that we find, and I write, as a test, the files to disk in a temp folder to check if the server can process binary files correctly. When you run this same code in a Netlify function everything seems to work, but you will get crippled files!

Processing multipart/form-data in NodeJS Azure function with HttpTrigger:

import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import * as Busboy from 'busboy';
import * as fs from 'fs';

function getTime() {
    return (new Date()).toISOString().substr(11, 8);
}

function log(s) {
    console.log(`${getTime()}: ${s}`);
}

const httpTrigger: AzureFunction = async function (context: Context, request: HttpRequest): Promise<void> {
    var busboy = new Busboy({ headers: request.headers });
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
        log(`File [${fieldname}]: filename: '${filename}', encoding: ${encoding}, mimetype: ${mimetype}`);
        file.on('data', function(data) {
          log(`File [${fieldname}]: filename: '${filename}', got ${data.length} bytes`);
          fs.writeFileSync('c:\\temp\\' + filename, data);
        });
        file.on('end', function() {
          log(`File [${fieldname}]: filename: '${filename}', Finished`);
        });
    });
    busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
        log(`Field [${fieldname}]: value: ${val}`);
    });
    busboy.on('finish', function() {
        log('Done parsing form!');
    
        context.res = {
            status: 200, 
            body: { result: "done" }
        };
    });
    busboy.write(request.body, function() {});
};

export default httpTrigger;

A sample for containing fields and image:

<form action="http://localhost:7071/api/handlePostData" method="post" enctype="multipart/form-data">
  <label for="fname">First name:</label>
  <input type="text" id="fname" name="fname"><br><br>
  <label for="lname">Last name:</label>
  <input type="text" id="lname" name="lname"><br><br>
  <label for="selfie">Selfie:</label>
  <input type="file" accept="image/*" capture id="selfie" name="selfie"><br><br>
  <input type="submit" value="Submit">
</form>

Check the Busboy documentation for more information on configuration options and the handling fields and files.

Discuss on TwitterEdit on GitHub

Serge van den Oever's weblog

Serge van den Oever

Personal blog by Serge van den Oever - als je maar lol hebt...