Home » Nodejs » How to cancel HTTP upload from data events?

How to cancel HTTP upload from data events?

Posted by: admin November 30, 2017 Leave a comment

Questions:

Given this simple webserver code:

console.log('starting');
var server = require('http').createServer();

server.on('connection',function(socket){console.log('*server/connection');});
server.on(
    'request',
    function(request, response){
        console.log('*server/request');
        request.on(
            'data',
            function(chunk){
                console.log('*request/data');
                // <!> How do I abort next data calls from here?
            }
        );
        request.on(
            'readable',
            function(chunk){
                console.log('*request/readable');
                // <!> How do I abort next readable calls from here?
            }
        );
        request.on(
            'end',
            function(){
                console.log('*request/end');
                response.writeHead(200,"OK");
                response.write('Hello');
                response.end();
            }
        );
        request.on('close',function(){ console.log('*request/close'); } );
        request.on('error',function(){ console.log('*request/error'); } );
    }
);
server.on('close',function(){console.log('server/close');});
server.on('checkContinue',function(request, response){console.log('*server/checkContinue');});
server.on('connect',function(request, socket, head){console.log('*server/connect');});
server.on('upgrade',function(request, socket, head){console.log('*server/upgrade');});
server.on('clientError',function(exception, socket){console.log('*server/clientError');});

server.listen(8080);
console.log('started');

When a POST or FILE is submited, my on data function is triggered one or more or several times. Sometimes (like an monstrous big file is sent) I want to cancel this on data events and trigger the on end function to user (later on I will display a “your post/file is too big”). How do I do it?

Answers:

The proper, spec-compliant thing to do here is simply send a HTTP 413 response early – that is, as soon as you detect that the client has sent more bytes than you want to handle. It is up to you whether or not you terminate the socket after sending the error response. This is in line with RFC 2616: (emphasis added)

413 Request Entity Too Large

The server is refusing to process a request because the request entity is larger than the server is willing or able to process. The server MAY close the connection to prevent the client from continuing the request.

What happens next is not ideal.

  • If you leave the socket open, all browsers (Chrome 30, IE 10, Firefox 21) will keep sending data until the entire file is uploaded. Then and only then, the browser will display your error message. This really sucks since the user must wait for the entire file to complete the upload, only to find out the server rejected it. It also wastes your bandwidth.

    The browsers’ current behavior is in violation of RFC 2616 § 8.2.2:

    An HTTP/1.1 (or later) client sending a message-body SHOULD monitor the network connection for an error status while it is transmitting the request. If the client sees an error status, it SHOULD immediately cease transmitting the body. If the body is being sent using a “chunked” encoding (section 3.6), a zero length chunk and empty trailer MAY be used to prematurely mark the end of the message. If the body was preceded by a Content-Length header, the client MUST close the connection.

    There are open Chrome and Firefox issues, but don’t expect a fix any time soon.

  • If you close the socket immediately after sending the HTTP 413 response, all browsers will obviously stop uploading immediately, but they most currently show a “connection reset” error (or similar), not any HTML you might send in the response.

    Again, this is probably a violation of the spec (which allows the server to send a response early and close the connection), but I wouldn’t expect browser fixes any time soon here either.

    Update: As of 4/15, Chrome may display your 413 HTML when you close the connection early. This only works when the browser is running on Linux and Mac OS X. On Windows, Chrome still displays ERR_CONNECTION_RESET network error rather than the HTML you sent. (IE 11 and Firefox 37 continue to just show a network error on all platforms.)

So your choices with traditional plain HTTP uploads are:

  • Show a friendly error message, but only after the upload runs to completion. This wastes time and bandwidth.

  • Fail fast, but leave users confused with a cryptic browser error screen.

Your best bet here is probably to use a AJAX uploader where you have more control over the user experience. You should still provide a tradtional upload form as a fallback, and I’d use the “fail fast” option (close the socket) to prevent wasted time and bandwidth.

Here’s some example code that kills a request if it receives more than 1 kB. I’m using Express, but the same should apply with node’s vanilla HTTP library.

Note: In reality, you should use formidable multiparty to process your uploads (it’s what Connect/Express uses), and it has its own way to monitor upload data.

var express = require("express")
    , app = express();

app.get('/', function(req, res) {
    res.send('Uploads &gt; 1 kB rejected<form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="submit"></form>');
});

app.post('/upload', function(req, res) {
    var size = 0;

    var gotData = function(d) {
        size += d.length; // add this chunk's size to the total number of bytes received thus far
        console.log('upload chunk', size);
        if (size > 1024) {
            console.log('aborting request');
            req.removeListener('data', gotData); // we need to remove the event listeners so that we don't end up here more than once
            req.removeListener('end', reqEnd);
            res.header('Connection', 'close'); // with the Connection: close header set, node will automatically close the socket...
            res.send(413, 'Upload too large'); // ... after sending a response
        }
    };

    var reqEnd = function() {
       res.send('ok, got ' + size + ' bytes');
    }

    req.on('data', gotData);

    req.on('end', reqEnd);
});

app.listen(3003);

Questions:
Answers:

The request parameter is an http.IncomingMessage class which does not allow to stop the stream.

But you have access to the underlying socket, and you can abort it:

request.socket.end('too big !');

But I’m not sure that the browser will enjoy it… He will probably complain and indicates that connection was impropertly closed.

Questions:
Answers:

Here is my solution:

var maxSize = 30 * 1024 * 1024;    //30MB
app.post('/upload', function(req, res) {

    var size = req.headers['content-length'];
    if (size <= maxSize) {
        form.parse(req, function(err, fields, files) {
            console.log("File uploading");
            if (files && files.upload) {
                res.status(200).json({fields: fields, files: files});
                fs.renameSync(files.upload[0].path, uploadDir + files.upload[0].originalFilename);
            }
            else {
              res.send("Not uploading");
            }
        });
    }
    else {
        res.send(413, "File to large");
    }

And in case of wasting client’s uploading time before get response, control it in the client javascript.

if (fileElement.files[0].size > maxSize) {
    ....
}

Questions:
Answers:

I use Formidable this way:

var formidable = require('formidable'),
    http = require('http'),
    util = require('util');

var MaxFieldSize = 1000 * 1000,
    MaxFields = 100,
    MaxUploadSize = 8 * 1000 * 1000;


http.createServer (function(req, res) {

    console.log (req.url);
    console.log (req.headers ["content-type"]);

    if (req.url == '/upload')
    {
        var form = new formidable.IncomingForm();

        form.maxFieldsSize = MaxFieldSize;
        form.maxFields = MaxFields;

        form.on ('progress', function (bytesReceived, bytesExpected) {
            //console.log (bytesReceived, bytesExpected);
            if (bytesReceived > MaxUploadSize)
            {
                console.log ('*** TOO BIG');

                // ***HACK*** see Formidable lib/incoming_form.js
                // forces close files then triggers error in form.parse below
                // bonus: removes temporary files
                // --> use throttling in Chrome while opening /tmp in nautilus
                //     and watch the files disappear
                form.__2big__ = true;
                form._error (new Error ('too big'));

                //req.connection.destroy (); --- moved to form.parse
            }
        });

        form.parse (req, function (err, fields, files) {
            if (err)
            {
                console.log ('*** A', err);
                try // just in case something is wrong with the connection, e.g. closed
                {
                    // might not get through?
                    if (form.__2big__)
                    {
                        res.writeHead (413, {"connection": 'close', "content-type": 'text/plain'});
                        res.end ('upload to big');
                    }
                    else
                    {
                        res.writeHead (500, {"connection": 'close', "content-type": 'text/plain'});
                        res.end ('something wrong');
                    }
                    req.connection.destroy ();
                }
                catch (err)
                {
                    console.log ('*** B', err);
                }
            }
            else
            {
                res.writeHead (200, {"content-type": 'text/plain'});
                res.write ('received upload:\n\n');
                //for (let f in files)
                //    console.log (f, files [f]);
                res.end (util.inspect ({fields: fields, files: files}));
            }
        });
    }
    else
    {
        res.writeHead (200, {"content-type": 'text/html'});
        res.end (
            '<html>\
                <head>\
                    <meta charset="UTF-8">\
                    <title>Test Formidable</title>\
                </head>\
                <body>\
                    <form action="/upload" method="POST" enctype="multipart/form-data">\
                        <input type="hidden" name="foo" value="1">\
                        <input type="text" name="fooh" value="2"><br>\
                        <input type="file" name="bar"><br>\
                        <input type="file" name="baz"><br>\
                        <input type="file" name="boo"><br>\
                        <button type="submit">Submit</submit>\
                    </form>\
                </body>\
            </html>'
        );
    }
}).listen(8080);


/* terminal:
    #> node upload
    /upload
    multipart/form-data; boundary=----WebKitFormBoundaryvqt1lXRmxeHLZtYi
    *** TOO BIG
    *** A Error: too big
        at IncomingForm.<anonymous> (/home/marc/Project/node/upload.js:33:18)
        at emitTwo (events.js:106:13)
        at IncomingForm.emit (events.js:191:7)
        at IncomingForm.write (/home/marc/Project/node/node_modules/formidable/lib/incoming_form.js:155:8)
        at IncomingMessage.<anonymous> (/home/marc/Project/node/node_modules/formidable/lib/incoming_form.js:123:12)
        at emitOne (events.js:96:13)
        at IncomingMessage.emit (events.js:188:7)
        at IncomingMessage.Readable.read (_stream_readable.js:387:10)
        at flow (_stream_readable.js:764:26)
        at resume_ (_stream_readable.js:744:3)
*/