node package manager


Built upon formaline's ultrafast parser, truly a formidable competitor.

PoorForm uses formaline's ultra-fast parser (QAP) to create a much simpler multi-part form parser.

It may be insignificantly faster than both formidable and formaline, but that's not the point.

The point is that it's a simple base to build upon, kitchen sink not included.

Truly a formidable competitor.

npm install poor-form

There are two tests.

The first walks a directory and checks the md5 sums of the files against the md5sums calculated by the server.

The second creates a few thousand form submissions where each form has one more byte than the previous form (an attempt to catch off-by-one errors).

git clone git://
cd poor-form
cp npm-shrinkwrap.bak.json npm-shrinkwrap.json
npm install --dev
node test/example-md5sum-service.js 4444 &
node test/md5sum-test.js .
node test/md5sum-bits-test.js

If you encounter any errors running the test, it's probably just an issue of dependencies (there's some instanceof magic that fails if any modules from file-api are installed twice), but the npmshrinkwrap.json should be preventing this.

Returns a PoorForm emitter instance if !req.complete and /multipart/.test(req.headers['content-type']).

Returns null otherwise - either it's not a multi-part form, or the form has already been parsed.

// Using Connect, for example 
app.use(function (req, res, next) {
  var poorForm = PoorForm.create(req)
    , fields = []
    , count = 0
    , curField
  if (!poorForm) {
    console.log("Either this was already parsed or it isn't a multi-part form");
  // poorForm.on('fieldstart', ...) 
  // ... 

Emitted each time a new field is encountered.

headers will contain all raw mime headers (with lower-cased keys) as well as a few shortcut keys

headers = {
    name: "foo-fieldname"             // parsed value from Content-Disposition 
  , filename: "big.bin"               // parsed value from Content-Disposition 
  , type: "application/json"          // Just the MIME-type of the Content-Type 
  , 'content-type': "application/json; charset=utf-8"
  , 'content-disposition': 'form-data; name="foo-fieldname"; filename="big.bin"'
  , ...                               // any other raw headers (usually none) 
poorForm.on('fieldstart', function (headers) {
  var tmpPath = '/tmp/upload-' + count + '.bin'
  count += 1;
  curField = {};
  if (headers.filename) {
    console.log('Probably a file and probably has a mime-type', headers.type);
    curField.fw = fs.createWriteStream(tmpPath);
    curField.tmpPath = tmpPath;
  } else {
    console.log('Probably a field without a mime-type', headers.type);
    curField.value = '';
  curField.totalBytes = 0;
  curField.headers = headers;

Emitted for each chunk of data that belongs to a field or file (no headers, whitespace, etc).

poorForm.on('fielddata', function (buffer) {
  if (curField.fw) {
    console.log('Just wrote', buffer.length, 'bytes of a file');
  } else {
    curField.value += buffer.toString('utf8');
  curField.totalBytes += buffer.length;

NOTE: It's very possible for a single field with very few bytes to come in with multiple chunks

Emitted when the current field or file has completed.

poorForm.on('fieldend', function () {
  var lastField = curField
  if (curField.fw) {
    curField.fw = undefined;
    console.log('Just wrote a file of ', curField.totalBytes, 'bytes');
    fs.rename(curField.tmpPath, '/tmp/' + curField.headers.filename, function () {
      console.log('Renamed', lastField.tmpPath, 'to', lastField.headers.filename);
  } else {
    console.log('Just received', + ':' + curField.value);
  curField = null;

Emitted when the end-of-form boundary has been encountered.

poorForm.on('formend', function () {
  res.end(JSON.stringify(fields, null, '  '));

Number of bytes received so far - including all headers, whitespace, form fields, and files.

req.on('data', function () {
  var ratio = poorForm.loaded /
    , percent = Math.round(ratio * 100)
  console.log(percent + '% complete (' + poorForm.loaded + ' bytes)');
  // might be 0, if is Infinity 

The total number of bytes in the form - the same as req.headers['content-length'].

console.log( + 'bytes received thus far');

NOTE: If the content encoding is chunked will be Infinity.

Example: An md5sum webservice

/*jshint strict:true node:true es5:true onevar:true laxcomma:true laxbreak:true eqeqeq:true immed:true latedef:true unused:true undef:true*/
(function () {
  "use strict";
  var connect = require('connect')
    , PoorForm = require('poor-form')
    , crypto = require('crypto')
    , port = process.argv[2] || 3000
    , server
    , app
  // An md5sum service 
  app = connect.createServer()
    .use(function (req, res, next) {
        var poorForm = PoorForm.create(req)
          , hash
          , info
          , hashes = []
        if (!poorForm) {
          console.log("Either this was already parsed or it isn't a multi-part form");
        poorForm.on('fieldstart', function (headers) {
          console.log('[fieldstart]', headers.filename ||;
          hash = crypto.createHash('md5');
          info = headers;
        poorForm.on('fielddata', function (chunk) {
        poorForm.on('fieldend', function () {
          info.md5sum = hash.digest('hex');
        poorForm.on('formend', function () {
          res.end(JSON.stringify({ "success": true, "result": hashes }));
  server = app.listen(port, function () {

There are a few derivations of the multipart/* type:

  • multipart/form-data (with dispositions form-data and file) W3 Spec
  • multipart/mixed (similar to the file disposition, but without a declared disposition or filename) W3 RFC 1341
  • multipart/alternative
  • multipart/digest
  • multipart/parallel

poor-form could very easily be adapted to handle these types as well. However, I don't know of any practical use for them at the moment.

If a form ends unexpectedly, curFile should be closed.

Needs an error for when a form writes past the end boundary

Needs a default size limit on headers (4k would be more than reasonable) before sending an error