0

I looked at several SO posts trying to find a way to make a Node.js server tell a client to stop uploading after a certain file size has been reached. The most promising of these is ed-ta's technique at Avoiding further processing on busyboy file upload size limit.

Per ed-ta, my Node.js server seemed to be doing what it should. The server sent the 455 status code as soon as the size limit was reached and stopped accepting any more data. Unfortunately, my client kept processing the file until it was completely done anyway. This is a less than ideal experience when the user tries to upload extremely large files since the client doesn't alert the user that the threshold is reached until the AJAX request is completely done.

How do I get the client to see the 455 status code in a timely manner?

I tried checking for the 455 status inside xhr.onreadystatechange, but I can't seem to find that information from inside onreadystatehange even if the server has already sent the 455 in the response 1. Also, the onreadystatechange event doesn't seem to trigger until after the entire file has been processed by the client anyway.

I have tried to simplify the problem by getting rid of the irrelevant details and my current demo code follows:

Server.js

// This code is based on 
// https://stackoverflow.com/questions/23691194/node-express-file-upload
//
// [1] - https://stackoverflow.com/questions/39681966/
// avoiding-further-processing-on-busyboy-file-upload-size-limit
//
// [2] - https://stackoverflow.com/questions/18310394/
// no-access-control-allow-origin-node-apache-port-issue
//
// [3] - https://stackoverflow.com/questions/39681966/
// avoiding-further-processing-on-busyboy-file-upload-size-limit
//
// [4] - https://stackoverflow.com/questions/44736327/
// node-js-cors-issue-response-to-preflight-
// request-doesnt-pass-access-control-c
var express = require('express');
var busboy = require('connect-busboy');
var fs = require('fs-extra');
var cors = require('cors');  // [4]

const app = express();

// See [2][4]
app.use(
  function(req, res, next) {
    res.setHeader("Access-Control-Allow-Origin", "null");
    res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST")
    next();
  }
);
app.options('*', cors());

app.use(
  busboy({ limits: { files: 1, fileSize: 500000000 } }) // [1]
);

app.post('/uploadEndpoint', function (req, res, next) {
  var fStream;
  req.pipe(req.busboy);

  req.busboy.on('file', function (fieldName, file, filename) {
    console.log("Uploading: " + filename);

    var destPath = './' + filename;
    fStream = fs.createWriteStream(destPath);
    file.pipe(fStream);

    // ed-ta [3]
    // Despite being asynchronous limit_reach 
    // will be seen here as true if it hits max size 
    // as set in file.on.limit because once it hits
    // max size the stream will stop and on.finish
    // will be triggered.
    var limit_reach = false;
    req.busboy.on('finish', function() {
      if(!limit_reach){
        res.send(filename + " uploaded");
      }
    });
    file.on('limit', function() {
      fs.unlink(destPath, function(){ 
        limit_reach = true;
        res.status(455).send("File too big.");
        console.log('Telling client to stop...');
      }); 
    });
  });
})

app.listen(8000);

test.html

<!DOCTYPE html>
<!-- 
This code is based off timdream and Basar at
https://stackoverflow.com/questions/6211145/
upload-file-with-ajax-xmlhttprequest

[1] - https://stackoverflow.com/questions/49692745/
express-using-multer-error-multipart-boundary-not-found-request-sent-by-pos
-->
<html>
<head>
<meta charset="utf-8" />
<script>
    function uploadFile() {
    var progress = document.getElementById("output");
    progress.innerText = "Starting";
    var fileCtl = document.getElementById("theFile");
    var file = fileCtl.files[0];
    var xhr = new XMLHttpRequest();

    // timdream
    var formData = new FormData();
    formData.append('theFile', file);

    xhr.upload.onprogress = (progressEvent) => {
        var percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
        );
        progress.innerText = 
        "Percent Uploaded: " + percentCompleted + "%";
    };
    xhr.onreadystatechange = function(e) {
        if (this.status === 455) {
        alert('Sorry, file was too big.');
        }
    };
    xhr.open('post', 'http://localhost:8000/uploadEndpoint', true);
    // Apparently this line causes issues Multipart Boundary not
    // found error [1]
    // xhr.setRequestHeader("Content-Type","multipart/form-data"); 

    // timdream
    xhr.send(formData);
    }
</script>
</head>
<body>
<input type="file" id="theFile" name="theName" /><br />
<div id="output">Upload Progress</div>
<input type="button" id="theButton"
onclick="uploadFile();" value="Send" />
</body>
</html>

1 - I could setup another endpoint on the Node.js server and use AJAX to poll that endpoint for the current status inside the client's onprogress but that seems like a kludgy solution that would waste bandwidth.

Shawn Eary
  • 684
  • 2
  • 7
  • 21
  • So far, this is the only possible solution I have found: https://www.npmjs.com/package/socketio-file-upload I unfortunately can't get the XMLHttpRequest abort, error and onreadystatechange events to fire in a timely manner on the client... Sadly, this comment doesn't actually answer the question because WebSocket uploads from the socketio-file-upload package are not the same thing as multipart/form-data posts. – Shawn Eary Dec 13 '21 at 21:43
  • I just tried switching my client to Axios and using a cancel token. Even when I call cancelTokenSource.cancel(), my client keeps going. This makes *no* sense... https://stackoverflow.com/questions/38329209/how-to-cancel-abort-ajax-request-in-axios https://stackoverflow.com/questions/55051917/cancel-file-upload-post-request-using-axios – Shawn Eary Jan 04 '22 at 17:04
  • Went back to XMLHttpRequest. Opened up a separate WebSockets channel to server. When the server sends back an abort request via the WebSockets channel, my client is able to to call abort on the instantiated XMLHttpRequest object: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort – Shawn Eary Jan 04 '22 at 20:27

1 Answers1

0

To get around the problem described above, I wound up using a separate WebSocket channel to send a message from the server back down to the client to tell the said client to stop the upload. I then called abort on the client's XMLHttpRequest object per the Mozilla docs.

Final sample code looks like this:

Server.js

// This code is based on 
// https://stackoverflow.com/questions/23691194/node-express-file-upload
//
// [1] - https://stackoverflow.com/questions/39681966/
// avoiding-further-processing-on-busyboy-file-upload-size-limit
//
// [2] - https://stackoverflow.com/questions/18310394/
// no-access-control-allow-origin-node-apache-port-issue
//
// [3] - https://stackoverflow.com/questions/39681966/
// avoiding-further-processing-on-busyboy-file-upload-size-limit
//
// [4] - https://stackoverflow.com/questions/44736327/
// node-js-cors-issue-response-to-preflight-
// request-doesnt-pass-access-control-c
var express = require('express');
var busboy = require('connect-busboy');
var fs = require('fs-extra');
var cors = require('cors');  // [4]
var ws = require('ws');
var WebSocketServer = ws.WebSocketServer;

var g_ws; 
// BEGIN FROM: https://www.npmjs.com/package/ws
const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
    g_ws = ws;
});
// END  FROM: https://www.npmjs.com/package/ws

const app = express();

// See [2][4]
app.use(
    function(req, res, next) {
        res.setHeader("Access-Control-Allow-Origin", "null");
        res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST")
        next();
    }
);
app.options('*', cors());

app.use(
    busboy({ limits: { files: 1, fileSize: 300000000 } }) // [1]
);

app.post('/uploadEndpoint', function (req, res, next) {
    var fStream;
    req.pipe(req.busboy);

    req.busboy.on('file', function (fieldName, file, fileNameObject) {
        var filename = fileNameObject.filename;
        console.log("Uploading: " + filename);

        var destPath = './' + filename;
        fStream = fs.createWriteStream(destPath);
        file.pipe(fStream);

        // ed-ta [3]
        // Despite being asynchronous limit_reach 
        // will be seen here as true if it hits max size 
        // as set in file.on.limit because once it hits
        // max size the stream will stop and on.finish
        // will be triggered.
        var limit_reach = false;
        req.busboy.on('finish', function() {
            var message;
            if(!limit_reach){
                message = 'success';
                res.send(filename + " uploaded");
            } else {
                message = 'TooBig';
            }
            g_ws.send(message);
        });
        file.on('limit', function() {
            fs.unlink(destPath, function(){ 
                limit_reach = true;
                res.status(455).send("File too big.");
                console.log('Telling client to stop...');

                // https://www.npmjs.com/package/ws
                g_ws.send("TooBig");
            }); 
        });
    });
})

app.listen(8000);

test.html

<!DOCTYPE html>
<!-- 
This code is based off timdream and Basar at
https://stackoverflow.com/questions/6211145/
upload-file-with-ajax-xmlhttprequest

[1] - https://stackoverflow.com/questions/49692745/
express-using-multer-error-multipart-boundary-not-found-request-sent-by-pos
-->
<html>
<head>
<meta charset="utf-8" />
<script>
function uploadFile() {
    var progress = document.getElementById("output");
    progress.innerText = "Starting";
    var fileCtl = document.getElementById("theFile");
    var file = fileCtl.files[0];
    var xhr = new XMLHttpRequest();

    // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
    const socket = new WebSocket('ws://localhost:8080');
    socket.addEventListener('message', function (event) {
        if (event.data === 'TooBig') {
            // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
            xhr.abort(); 
            alert('Server says file was too big.');
        } else if (event.data === 'success') {
            alert('File uploaded sucessfully.');
        } else {
            alert('Unknown server error');
        }
        socket.close();
    });

    // timdream
    var formData = new FormData();
    formData.append('theFile', file);

    xhr.upload.onprogress = (progressEvent) => {
        var percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
        );
        progress.innerText = 
        "Percent Uploaded: " + percentCompleted + "%";
    };
    xhr.onreadystatechange = function(e) {
        if (this.status === 455) {
        alert('Sorry, file was too big.');
        }
    };
    xhr.open('post', 'http://localhost:8000/uploadEndpoint', true);
    // Apparently this line causes issues Multipart Boundary not
    // found error [1]
    // xhr.setRequestHeader("Content-Type","multipart/form-data"); 

    // timdream
    xhr.send(formData);
}
</script>
</head>
<body>
<input type="file" id="theFile" name="theName" /><br />
<div id="output">Upload Progress</div>
<input type="button" id="theButton"
onclick="uploadFile();" value="Send" />
</body>
</html>

This is not the exact code I used for my solution but this simplified working demo illustrates the concept.

BTW: Filesize limit was lowered to 300000000 on the server to make testing easier but that doesn't matter.

Shawn Eary
  • 684
  • 2
  • 7
  • 21