Jakemmarsh Jakemmarsh - 22 days ago 6
Node.js Question

Errors when posting from AngularJS to Amazon S3

I'm building an app on the MEAN (MongoDB, Express, AngularJS, node.js) stack that requires uploading image files to Amazon S3. I'm doing it in the following way:

First, an http

get
is sent to my API, which specifies the 'policy document' for the interaction and returns it to the AngularJS frontend. That backend code looks like this (with the variables filled in):

exports.S3Signing = function(req, res) {
var bucket = "MY_BUCKET_NAME",
awsKey = "MY_AWS_KEY",
secret = "MY_SECRET",
fileName = req.params.userId,
expiration = new Date(new Date().getTime() + 1000 * 60 * 5).toISOString();

var policy = {
"expiration": expiration,
"conditions": [
{"bucket": bucket},
{"key": fileName},
{"acl": 'public-read'},
["starts-with", "$Content-Type", ""],
["content-length-range", 0, 524288000]
]};

policyBase64 = new Buffer(JSON.stringify(policy), 'utf8').toString('base64');
signature = crypto.createHmac('sha1', secret).update(policyBase64).digest('base64');
res.json({bucket: bucket, awsKey: awsKey, policy: policyBase64, signature: signature});
};


The frontend (AngularJS) code for this process looks like this:

$http.get('/api/v1/auth/signS3/' + userId).success(function(data, status) {
var formData = new FormData();
formData.append('key', userId);
formData.append('AWSAccessKeyId', data.awsKey);
formData.append('acl', 'public-read');
formData.append('policy', data.policy);
formData.append('signature', data.signature);
formData.append('Content-Type', image.type);
formData.append('file', image);

$http({
method: 'POST',
url: 'http://' + data.bucket + '.s3.amazonaws.com/',
headers: {'Content-Type': 'multipart/form-data'},
data: formData
}).success(function(data) {
deferred.resolve(data);
}).error(function(err) {
deferred.reject(err);
});
}).error(function(err, status) {
deferred.reject(err);
});


However, as it is currently, if I post an image I get the following error:

Malformed POST Request The body of your POST request is not well-formed multipart/form-data.


If I change the http POST call to the AngularJS shorthand and remove the "content-type": "multipart/form-data" specification, like such:

$http.post('http://' + data.bucket + '.s3.amazonaws.com/', formData).success(function(data) {
deferred.resolve(data);
}).error(function(err) {
deferred.reject(err);
});


I get the following error instead:

Precondition Failed At least one of the pre-conditions you specified did not hold Bucket POST must be of the enclosure-type multipart/form-data


I have tried any combination of conditions and specifications that I can think of. What am I missing here?

Answer

I believe the problem was with the format of the "file" I was attempting to upload. I was using this AngularJS directive, and the format it returns resized images in isn't as an actual file. I'm still using that same directive, but no longer using the image resizing.

I changed the structure of my upload process to do most of the work on the node.js backend. The AngularJS function looks like this:

uploadImage: function(image, userId) {
            var deferred = $q.defer(),
                formData = new FormData();

            formData.append('image', image, image.name);

            $http.post('/api/v1/user/' + userId + '/image', formData, {
                transformRequest: angular.identity,
                headers: { 'Content-Type': undefined }
            }).success(function(data, status) {
                deferred.resolve(data);
            }).error(function(err, status) {
                deferred.reject(err);
            });

            return deferred.promise;
        }

And the node.js/express endpoint to post looks like this:

exports.uploadImage = function(req, res) {
    postToS3 = function(image, userId) {
        var s3bucket = new AWS.S3({params: {Bucket: config.aws.bucket}}),
            deferred = Q.defer(),
            getExtension = function(filename) {
                var i = filename.lastIndexOf('.');
                return (i < 0) ? '' : filename.substr(i);
            },
            dataToPost = {
                Bucket: config.aws.bucket,
                Key: 'user_imgs/' + userId + getExtension(image.name),
                ACL: 'public-read',
                ContentType: image.type
            };

        fs.readFile(image.path, function(err, readFile) {
            dataToPost.Body = readFile;

            s3bucket.putObject(dataToPost, function(err, data) {
                if (err) {
                  deferred.reject(err.message);
                } else {
                  deferred.resolve(data);
                }
            });
        });

        return deferred.promise;
    };

    postToS3(req.files.image, req.params.userId).then(function(data) {
        res.json(200, data);
    }, function(err) {
        res.send(500, err);
    })
};

This is now using Amazon's AWS sdk for node.js.