1

I'm new to node.js. What I'm trying to do is to stream the upload of a file from web browser to a cloud storage through my node.js server.

I'm using 'express', 'request' and 'busboy' modules.

var express = require("express");
var request = require("request");
var BusBoy = require("busboy");
var router = express.Router();

router.post("/upload", function(req, res, next) {
    var busboy = new BusBoy({ headers: req.headers });
    var json = {};

    busboy.on("file", function (fieldname, file, filename, encoding, mimetype) {
        file.on("data", function(data) {
            console.log(`streamed ${data.length}`);
        });

        file.on("end", function() {
            console.log(`finished streaming ${filename}`);
        });
        
        var r = request({
            url: "http://<my_cloud_storage_api_url>",
            method: "POST",
            headers: {
                "CUSTOM-HEADER": "Hello",
            },
            formData: {
                "upload": file
            }
        }, function(err, httpResponse, body) {
            console.log("uploaded");
            json.response = body;
        });
    });

    busboy.on("field", function(name, val) {
        console.log(`name: ${name}, value: ${value}`);
    });
    
    busboy.on("finish", function() {
        res.send(json);
    });

    req.pipe(busboy);
});

module.exports = router;

But I keep getting the following error on the server. What am I doing wrong here? Any help is appreciated.

Error: Part terminated early due to unexpected end of multipart data
at node_modules\busboy\node_modules\dicer\lib\Dicer.js:65:36
at nextTickCallbackWith0Args (node.js:420:9)
at process._tickCallback (node.js:349:13)
alex
  • 614
  • 1
  • 6
  • 15

1 Answers1

1

I realize this question is some 7 months old, but I shall answer it here in an attempt help anyone else currently banging their head against this.

You have two options, really: Add the file size, or use something other than Request.

Note: I edited this shortly after first posting it to hopefully provide a bit more context.

Using Something Else

There are some alternatives you can use instead of Request if you don't need all the baked in features it has.

  • form-data can be used by itself in simple cases, or it can be used with, say, got. request uses this internally.
  • bhttp advertises Streams2+ support, although in my experience Streams2+ support has not been an issue for me. No built in https support, you have to specify a custom agent
  • got another slimmed down one. Doesn't have any special handling of form data like request does, but is trivially used with form-data or form-data2. I had trouble getting it working over a corporate proxy, though, but that's likely because I'm a networking newb.
  • needle seems pretty light weight, but I haven't actually tried it.

Using Request: Add the File Size

Request does not (as of writing) have any support for using transfer-encoding: chunked so to upload files with it, you need to add the file's size along with the file, which if you're uploading from a web client means that client needs to send that file size to your server in addition to the file itself.

The way I came up with to do this is to send the file metadata in its own field before the file field.

I modified your example with comments describing what I did. Note that I did not include any validation of the data received, but I recommend you do add that.

var express = require("express");
var request = require("request");
var BusBoy = require("busboy");
var router = express.Router();

router.post("/upload", function(req, res, next) {
    var busboy = new BusBoy({ headers: req.headers });
    var json = {};

    // Use this to cache any fields which are file metadata.
    var fileMetas = {};

    busboy.on("file", function (fieldname, file, filename, encoding, mimetype) {
        // Be sure to match this prop name here with the pattern you use to detect meta fields.
        var meta = fileMetas[fieldname + '.meta'];

        if (!meta) {
            // Make sure to dump the file.
            file.resume();
            // Then, do some sort of error handling here, because you cannot upload a file
            // without knowing it's length.
            return;
        }

        file.on("data", function(data) {
            console.log(`streamed ${data.length}`);
        });

        file.on("end", function() {
            console.log(`finished streaming ${filename}`);
        });

        var r = request({
            url: "http://<my_cloud_storage_api_url>",
            method: "POST",
            headers: {
                "CUSTOM-HEADER": "Hello",
            },
            formData: {
                // value + options form of a formData field.
                "upload": {
                    value: file,
                    options: {
                        filename: meta.name,
                        knownLength: meta.size
                    }
                }
            }
        }, function(err, httpResponse, body) {
            console.log("uploaded");
            json.response = body;
        });
    });

    busboy.on("field", function(name, val) {
        // Use whatever pattern you want.  I used (fileFieldName + ".meta").
        // Another good one might be ("meta:" + fileFieldName).
        if (/\.meta$/.test(name)) {
            // I send an object with { name, size, type, lastModified },
            // which are just the public props pulled off a File object.
            // Note: Should probably add error handling if val is somehow not parsable.
            fileMetas[name] = JSON.parse(val);
            console.log(`file metadata: name: ${name}, value: ${value}`);
            return;
        }

        // Otherwise, process field as normal.
        console.log(`name: ${name}, value: ${value}`);
    });

    busboy.on("finish", function() {
        res.send(json);
    });

    req.pipe(busboy);
});

module.exports = router;

On the client, you need to then send the metadata on the so-named field before the file itself. This can be done by ordering an <input type="hidden"> control before the file and updating its value onchange. The order of values sent is guaranteed to follow the order of inputs in appearance. If you're building the request body yourself using FormData, you can do this by appending the appropriate metadata before appending the File.

Example with <form>

<script>
    function extractFileMeta(file) {
        return JSON.stringify({
            size: file.size,
            name: file.name,
            type: file.type,
            lastUpdated: file.lastUpdated
        });
    }

    function onFileUploadChange(event) {
        // change this to use arrays if using the multiple attribute on the file input.
        var file = event.target.files[0];
        var fileMetaInput = document.querySelector('input[name=fileUpload.meta]');

        if (fileMetaInput) {
            fileMetaInput.value = extractFileMeta(file);
        }
    }
</script>
<form action="/upload-to-cloud">
    <input type="hidden" name="fileUpload.meta">
    <input type="file" name="fileUpload" onchange="onFileUploadChange(event)">
</form>

Example with FormData:

function onSubmit(event) {
    event.preventDefault();

    var form = document.getElementById('my-upload-form');
    var formData = new FormData();

    var fileUpload = form.elements['fileUpload'];
    var fileUploadMeta = JSON.stringify({
        size: fileUpload.size,
        name: fileUpload.name,
        type: fileUpload.type,
        lastUpdated: fileUpload.lastUpdated
    });

    // Append fileUploadMeta BEFORE fileUpload.
    formData.append('fileUpload.meta', fileUploadMeta);
    formData.append('fileUpload', fileUpload);

    // Do whatever you do to POST here.
}
Joseph Sikorski
  • 715
  • 6
  • 14