I'm implementing a file uploader where a user can upload one or more files, using XMLHttpRequest
. I'm not using fetch
as I need to be able to provide visual feedback on upload progress to the user.
The problem I'm having occurs when the server stops processing the upload before it has completed (for example closing the connection with a 413 Payload Too Large
error if the file(s) being uploaded are too large). If an error like this occurs when using Safari or Chrome, they will halt the upload as I intend.
In Firefox, however, it seemingly ignores this and retries the upload several times before stopping.
My code is as follows:
// Initialize a new request object.
let req = new XMLHttpRequest();
// Set expected response as JSON.
req.responseType = 'json';
// Set event handlers.
req.upload.onreadystatechange = function(e) { console.log(e.type); }
req.upload.onuploadstart = function(e) { console.log(e.type); }
req.upload.onprogress = function(e) { console.log(e.type); }
req.upload.onabort = function(e) { console.log(e.type); }
req.upload.onload = function(e) { console.log(e.type); }
req.upload.ontimeout = function(e) { console.log(e.type); }
req.upload.onuploadend = function(e) { console.log(e.type); }
// Open request, set request header.
req.open('POST', '/some-endpoint', true);
req.setRequestHeader('Content-type', 'multipart/form-data;boundary=---some-boundary---');
// Create FormData object to submit.
let fd = new FormData(formElement);
// Send data.
req.send(fd);
In Safari and Chrome, when I upload a file that is too large for the server to accept, resulting in the server closing the connection with a 413 status response, events are fired in the following order:
loadstart
progress (multiple)
Failed to load resource (413 Request Entity Too Large)
as I expected. In Firefox, events are fired in the following order:
loadstart
progress (multiple, ignoring connection closes and restarting upload multiple times)
loadend
Firefox does not seem to fire a load
, error
, abort
, or timeout
event before the loadend
event, as indicated in the XMLHttpRequest.upload
documentation
Looking at the network tabs of each of the browsers' dev tools indicates that Chrome and Safari both recognise that the server has responded with 413, but Firefox has not recognised any response status (even after loadend
).
Versions are Firefox Quantum 62.0b3 (64-bit). Safari is 11.0.1. Chrome is 67.0.3396.99.
So, the question is: Why is Firefox unable to recognise that a server error has occured during an upload, and cancel the upload, where Safari and Chrome can? and Is there a way I can resolve this?
As Cody G. suggested this might be a bug, or related to a bug, in Firefox.
This does not answer the original question. However, it does provide a workaround, and hopefully some potentially illuminating information for anyone else.
Firefox, Safari, and Chrome all fire events in the same order when the upload is successful (i.e. when the server does not send back a response or close the connection before upload is completed). That order is:
readystatechange (readyState = 1)
loadstart
progress (1...n times)
load
loadend
readystatechange (readyState = 2)
readystatechange (readyState = 4)
...as expected.
Safari and Chrome fire events in the same order when the upload fails (i.e. when the server closes the connection and sends back a response). That order is:
readystatechange (readyState = 1)
loadstart
progress (1...n times)
[the server responds with an error, which does *not* trigger an error event]
readystatechange (readyState = 2)
readystatechange (readyState = 3)
readystatechange (readyState = 4)
Firefox, on the other hand, fires events in this order when the upload fails:
readystatechange (readyState = 1)
loadstart
progress (1...n times, including retrying from the start more than once when the server responds or closes the connection)
readystatechange (readyState = 2)
readystatechange (readyState = 3)
readystatechange (readyState = 4)
error
loadend
My workaround to prevent Firefox from restarting the upload potentially multiple times for no reason is to include a variable that keeps track of the previously loaded amount:
let prevLoaded = 0;
xhr.upload.addEventListener('progress', function(e) {
if (prevLoaded !== 0 && e.loaded <= prevLoaded) {
xhr.abort();
return;
}
prevLoaded = e.loaded;
}, false);
This causes the request to be cancelled. Safari and Chrome won't run this code, as other events fire before they can. With this code in place, Firefox's event firing order for unsuccessful uploads becomes:
readystatechange (readyState = 1)
loadstart
progress (1...n times, but almost always stopping after the server closes the connection or responds with an error)
readystatechange (readyState = 4)
abort
loadend