Skip to content

Commit

Permalink
Node 4.3.1 or higer is required for proper multipart parse
Browse files Browse the repository at this point in the history
Until Node commit nodejs/node#4738 Buffer.byteLength function did not work correctly. (This commit was entered in Node 4.3.1 https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V4.md#4.3.1)
On hapijs/pez project, the commit hapijs/pez@9f0042b#diff-6d186b954a58d5bb740f73d84fe39073R162 was depending on Buffer.byteLength to work correctly.
This resulted in a misbehaving multipart parsing when the payload maxBytes is close to the uploaded binary file (Inside the multipart form post payload).
Meaning that Subtext library parsing a multipart payload with a binary file that its content-length is smaller than the maxBytes parameter but is close enough than the inner hapijs/pez libary will count mistakenly for a bigger buffer size and will return an error "Maximum size exceeded" (Subtext will return "Invalid multipart payload format").

I wrote a little program that will help you understand the described scenario.
Notice: 
1. You need to put a binary file inside the project for the program to work.
2. In my example the file was 28MB and the limit I entered was 30MB.

Package.json:
{
  "name": "subtext_example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "hapi": "^13.4.1",
    "restler": "^3.4.0",
    "subtext": "^4.0.4"
  }
}


Index.json
'use strict';

const Hapi = require('hapi');
const Subtext = require('subtext');
const Restler = require('restler');
const Fs = require('fs');
const Path = require('path');

const server = new Hapi.Server();
server.connection({ port: 3000 });

var demoFilePath = Path.join(__dirname,"binary_file.exe");
var demoFileMimeType = "application/x-msdownload";
var postUrl = "http://localhost:3000/test_subtext";
var payloadMaxBytes = (30 * 1024 * 1024);

server.route({
    method: 'POST',
    path: '/test_subtext',
    config : {
        payload : {
            output: 'stream',
            parse : false,
            timeout : false,
            maxBytes : (1024 * 1024 * 1024)
        }
    },
    handler: function (request, reply) {
        var subtextParseOptions = {
                output: 'file',
                parse : true,
                timeout : false,
                maxBytes : payloadMaxBytes
            };

        const onParsed = (err, parsed) => {

            request.mime = parsed.mime;
            request.payload = parsed.payload || null;

            if (!err) {
                return reply();
            }

            const failAction = request.route.settings.payload.failAction;         // failAction: 'error', 'log', 'ignore'
            if (failAction !== 'ignore') {
                request._log(['payload', 'error'], err);
            }

            if (failAction === 'error') {
                return reply(err);
            }

            return reply();
        };

        request._isPayloadPending = true;
        Subtext.parse(request.raw.req, request._tap(), subtextParseOptions, (err, parsed) => {

            if (!err ||
                !request._isPayloadPending) {

                request._isPayloadPending = false;
                return onParsed(err, parsed);
            }

            // Flush out any pending request payload not consumed due to errors

            const stream = request.raw.req;

            const read = () => {

                stream.read();
            };

            const end = () => {

                stream.removeListener('readable', read);
                stream.removeListener('error', end);
                stream.removeListener('end', end);

                request._isPayloadPending = false;
                return onParsed(err, parsed);
            };

            stream.on('readable', read);
            stream.once('error', end);
            stream.once('end', end);
        });
    }
});

server.start((err) => {
    Fs.stat(demoFilePath, function(err, stats) {
        Restler.post(postUrl, {
            multipart: true,
            data: {
                "firstKey": "firstValue",
                "filename": Restler.file(demoFilePath, null, stats.size, null, demoFileMimeType)
            }
        }).on("complete", function(data) {
            console.log('Completed: ' + JSON.stringify(data));
        });
    });
});
  • Loading branch information
shugigo authored Jun 10, 2016
1 parent 19c43ae commit 5e1db35
Showing 1 changed file with 1 addition and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"multipart"
],
"engines": {
"node": ">=4.0.0"
"node": ">=4.3.1"
},
"dependencies": {
"boom": "3.x.x",
Expand Down

0 comments on commit 5e1db35

Please sign in to comment.