Search code examples
javascriptajaxnode.jsformshttp-post

NodeJS - How to parse multipart form data without frameworks?


I'm trying to do a basic thing: to send a form using FormData API and parse it in NodeJS.

After searching SO for an hour only to find answers using ExpressJS and other frameworks I think it deserves its own question:

I have this HTML:

<form action="http://foobar/message" method="POST">
  <label for="message">Message to send:</label>
  <input type="text" id="message" name="message">
  <button>Send message</button>
</form>

JS:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://foobar/message');
xhr.send(new FormData(form));

In NodeJS I'm doing:

var qs = require('querystring');

var requestBody = '';
request.on('data', function (chunk) {
  requestBody += chunk;
});
request.on('end', function () {
  var data = qs.parse(requestBody);
  console.log(data.message);
});

But in data.message I get the Webkit Boundary thing (from the multipart form data format) instead of the expected message. Is there another built-in lib to parse multipart post data instead of querystring? If not then how to do it manually (high-level, without reading the source code of Express)?


Solution

  • You Buffer.from POST body to string then .split it by the boundary value provided in the Content-Type: request header. This gives your body parts in an array.

    Now process them to determine which is a file and which is a key:val pair. The below code illustrates this.

    NodeJS / Server Side Example

    const SERVER = http.createServer(async function(request, response) {
      let statusCode = 200;
      if(request.url === '/app') {
        let contentTypeHeader = request.headers["content-type"];
        let boundary = "--" + contentTypeHeader.split("; ")[1].replace("boundary=","");
        if (request.method == 'POST') {
          let body = [];
          request.on('data', chunk => {
            body.push(chunk)
          });
          request.on('end', async () => {
            body = Buffer.concat(body).toString();
            let bodyParts = body.split(boundary);
            let result = [];
            bodyParts.forEach(function(val,index){
              val = val.replace("Content-Disposition: form-data; ","").split(/[\r\n]+/);
              if(isFile(val)){
                result.push(returnFileEntry(val))
              }
              if(isProperty(val)){
                result.push(returnPropertyEntry(val))
              }
            })
            console.log(result)
          });  
          response.end();  
        }
        response.end();
    

    Then the processing functions

    
    function returnPropertyEntry(arr){
      if (!Array.isArray(arr)) {return false};
      let propertyName = '';
      let propertyVal = undefined;
      arr.forEach(function(val,index){
        if(val.includes("name=")){
          propertyName = arr[index].split("name=")[1];
          propertyVal = arr[index + 1]
        }
      })
      return [propertyName,propertyVal];
    }
    
    function returnFileEntry(arr){
      if (!Array.isArray(arr)) {return false};
      let fileName = '';
      let file = undefined;
      arr.forEach(function(val,index){
        if(val.includes("filename=")){
          fileName = arr[index].split("filename=")[1];
        }
        if(val.toLowerCase().includes("content-type")){
          file = arr[index + 1];
        }
      })
      return [fileName,file];
    }
    function isFile(part){
      if(!Array.isArray(part)){return false};
      let filenameFound = false;
      let contentTypeFound = false;
      part.forEach(function(val,index){
        if (val.includes("filename=")){
          filenameFound = true;
        }
        if (val.toLowerCase().includes("content-type")){
          contentTypeFound = true;
        }
      });
      part.forEach(function(val,index){
        if (!val.length){
          part.splice(index,1)
        }
      });
      if(filenameFound && contentTypeFound){
        return part;
      } else {
        return false;
      }
    }
    function isProperty(part){
      if(!Array.isArray(part)){return false};
      let propertyNameFound = false;
      let filenameFound = false;
      part.forEach(function(val,index){
        if (val.includes("name=")){
          propertyNameFound = true;
        }
      });
      part.forEach(function(val,index){
        if (val.includes("filename=")){
          filenameFound = true;
        }
      });
      part.forEach(function(val,index){
        if (!val.length){
          part.splice(index,1)
        }
      });
      if(propertyNameFound && !filenameFound){
        return part;
      } else {
        return false;
      }
    }
    

    enter image description here