Skip to content
Built upon formaline's ultrafast parser, truly a formidable competitor.
Find file
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


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', ...)
  // ...

PoorForm#on('fieldstart', function (headers) { ... })

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;

PoorForm#on('fielddata', function (buffer) { ... })

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

PoorForm#on('fieldend', function () { ... })

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;

PoorForm#on('formend', function () { ... })

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 () {


Possible Future Enhancements

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

Something went wrong with that request. Please try again.