1

While creating an Ajax request for a user-uploaded image, I'm using a FormData() instance in the following manner:

var form_data = new FormData();
form_data.append(img_field, img_to_send, img_name);

Note that img_to_send is an object of type Blob.

My problem is this little browser compatibility caveat in the FormData MDN web docs:

XHR in Android 4.0 sends empty content for FormData with blob.

This relates to the Android browser (ver 4.0). This implies the xhr I'm attempting with FormData via append is probably going to fail in that particular browser.

My question is, what kind of alternative can I utilize to ensure the said browser is able to correctly process the xhr (a requirement of mine)? It'll be great to get an illustrative example.

And if there are no alternatives here, how do I write my code such that it only executes for browsers that support append with a blob object? Something like this?

if (window.FormData.append) {
// xhr code comes here
} 

I'm quite unsure about that one.

p.s. please stick to pure JS for the purposes of answering the question.

Community
  • 1
  • 1
Hassan Baig
  • 15,055
  • 27
  • 102
  • 205

2 Answers2

1

... Not an easy task...

First question I would ask myself is if I really have to support this 7 years old browser with 0% of usage?

Feature detection:

We can now since quite recently check what a FormData contains through its get(), has(), entries(), values() etc. methods. But the first specifications and implementations didn't had these methods and hence didn't offer any mean to detect this particular flow.

I don't have such an Android 4 browser to check, but I guess they didn't had any of these methods either...

An other modern and a bit hackish way to check would be to use a ServiceWorker, which should be able to intercept a dummy request, letting you know if your Blob was well appended, but once again, ServiceWorkers didn't exist 7 years ago.

This leaves us with an ugly browser identification, rather than a feature-detection (e.g navigator.userAgent parsing). I can't advice doing so because it's so prone to err that it might be better just letting your user know it failed in your server response.

Workaround

The ability to send a generated Blob as binary through an other mean than FormData has only appeared a few months ago, and currently only Blink browsers do support it natively, and FF through a bug exploit.

This means that the only workaround for your users on Android Browser 4.xxx and for all the browsers that didn't support this method, would be to save the generated Blob on their device, and then to pick it from an <input> and send it through a normal HTML <form>, but this is assuming they will be able to even save this Blob on their device, and I can't tell for sure...

Or you could maybe send a 30% bigger base64 representation of this data.

Or, probably the best is to let them know they should update their browser because it's really dangerous to have such an old browser facing the web nowadays.


So a small recap of the less bad possibilities:

  1. Check if FormData is available. Otherwise fallback to an ugly b64 in a <form>
  2. Send a first time with optimal Blob as multi-part.
  3. On server: check if the received Blob is empty. In this case, let the front-side know from a custom response.
  4. If server said it failed, send again, this time as b64. Since the first time was empty, it should not have been a too heavy request anyway.
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Well a sprinkling of users I'm dealing with (less than 1%) actually use `4.0.4` and `4.0.3`. So I thought it's worth a shot asking the question, in case there's a convenient fix for this. Seems there isn't. I suppose the best way would be to send base64 (in case of `v4`). But then it seems from your answer that it's tough to ascertain `v4` (or less) with precision. That means no real "good" solution exists here. But thanks for the answer, I'll mark it correct in a bit. – Hassan Baig Feb 15 '18 at 21:58
  • Would have been great if one could have checked whether the blob object appended correctly to the FormData object, based on which, one could have processed a fall back. – Hassan Baig Feb 15 '18 at 23:27
  • Well if you have access to the server code, you can do the check there. If the received file is empty, let the frontside know with a special response and try again in b64. – Kaiido Feb 15 '18 at 23:55
  • Yea, you're right, that's one way to do it. Thanks. – Hassan Baig Feb 16 '18 at 00:00
1

Hello this is a little example how to send file to server as BINARY STRING. With this you not requrired a formData. You can send with simple POST. Please change the url uploadFile.php to your URL. And read the comments about variables example that your server should receiving.

     <!DOCTYPE html>

<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">


    </head>
    <body>
        <div>
            <input id="btnFile" type="file" accept="image/*" />
        </div>
        <div style="margin-top: 20px; width: 100px; border: solid 1px red">
            <div id="divProgress" style="background-color: red;width: 10px; height: 5px; margin: 1px"></div>
        </div>
        <div style="margin-top: 20px">
            <input id="btnSend" type="button" value="Send File" />
            <input id= "user_id" type="hidden" value="123"/>
        </div>

        <script>



            var btnFile = document.getElementById("btnFile");
            var btnSend = document.getElementById("btnSend");
            var divProgress = document.getElementById("divProgress");



            var selectedFile = null;

            //Register event on file selected or changed.
            addEvents(btnFile, "change", function (event) {
                if (event.target.files.length !== 0) {
                    var file = event.target.files[0];
                    //Check if the file is IMAGE.
                    if (file.type.match("image.*")) {
                        selectedFile = file;
                    } else {
                        selectedFile = null;
                        alert("Please select a IMAGE FILE");
                    }
                } else {
                    selectedFile = null;
                }

            });

            //EVENT BTN SEND.
            addEvents(btnSend, "click", function () {
                if (selectedFile === null) {
                    //Please select a file to upload.
                    alert("Please select the file.");
                    return;
                }

                //File reader object.
                var fl = new FileReader();

                //Add event to read the content file.
                addEvents(fl, "load", function (evt) {
                    //alert(evt.target.result);
                    try {


                        //CONVERT ARRAY BUFFER TO BASE64 STRING.
                        var binaryString = evt.target.result;

                        //NOW YOU CAN SEND SIMPLE POST DATA.
                        var xhr = new XMLHttpRequest();

                        if (supportProgress(xhr)) {
                            addEvents(xhr, "progress", onXHRProgress);
                            addEvents(xhr, "loadstart", onXHRLoadStart);
                            addEvents(xhr, "abort", onXHRAbort);
                        }


                        xhr.open("POST", "/uploadFile.php", true);
                        //xhr.setRequestHeader("Content-Type", "application/json");

                        var user_id = document.getElementById('user_id').value;

                        var myData = {
                            uid: user_id,
                            fileName: selectedFile.name,
                            mimeType: selectedFile.type,
                            extension: getFileExtension(selectedFile),
                            contentFile: binaryString

                        };
                        xhr.send(JSON.stringify(myData));

                        /*
                         * IN YOUR SERVER SIDE YOU GET THE POST VARIABLE.
                         * fileName = The name of the file.
                         * mimeType = example "image/png"
                         * extension = png
                         * conentFile = Binary String of the content file and you can convert the Binary String to File in your disk according extension or mimeType
                         */

                    } catch (e) {

                    }

                });

                //Read the file as arraybuffer.
                fl.readAsBinaryString(selectedFile);
            });


            function onXHRProgress(e) {
                var loaded = 0;
                if (e.lengthComputable) {
                    if (e.loaded === e.total) {
                        loaded = 100;
                        selectedFile = null;
                    } else {
                        loaded = Math.round((e.loaded * 100) / e.total);
                    }

                    //Change the progress here.
                    divProgress.style.width = loaded + "px";
                }
            }

            function onXHRLoadStart() {
                divProgress.style.width = "0px";
            }

            function onXHRAbort() {
                selectedFile = null;

            }



            function getFileExtension(file) {
                var fileName = file.name;
                var i = fileName.toString().lastIndexOf(".");
                if (i !== -1) {
                    return fileName.toString().substring((i + 1), fileName.toString().length).toLowerCase();
                } else {
                    return "";
                }

            }


            function supportProgress(xhr) {
                return !!(xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
            }

            function addEvents(obj, evtName, func) {
                if (obj.addEventListener !== undefined && obj.addEventListener !== null) {
                    obj.addEventListener(evtName, func, false);
                } else if (obj.attachEvent !== undefined && obj.attachEvent !== null) {
                    obj.attachEvent(evtName, func);
                } else {
                    if (this.getAttribute("on" + evtName) !== undefined) {
                        obj["on" + evtName] = func;
                    } else {
                        obj[evtName] = func;
                    }
                }

            }

            function removeEvents(obj, evtName, func) {
                if (obj.removeEventListener !== undefined && obj.removeEventListener !== null) {
                    obj.removeEventListener(evtName, func, false);
                } else if (obj.detachEvent !== undefined && obj.detachEvent !== null) {
                    obj.detachEvent(evtName, func);
                } else {
                    if (this.getAttribute("on" + evtName) !== undefined) {
                        obj["on" + evtName] = null;
                    } else {
                        obj[evtName] = null;
                    }
                }

            }






        </script>
    </body>
</html>
toto
  • 1,180
  • 2
  • 13
  • 30
  • This is cool, but here's a question. I also need to send other parameters alongwith user uploaded image. For instance, an image `caption` and a user `id`. Currently, I'm simply using `form_data.append("uid", document.getElementById('user_id').value);` for user_id (and likewise for caption). How would that be possible via using this solution? – Hassan Baig Feb 16 '18 at 01:22
  • in the line: xhr.send(JSON.stringify({fileName: selectedFile.name, mimeType: selectedFile.type, extension: getFileExtension(selectedFile), contentFile: base64String})); you can add more variable to JSON object. – toto Feb 16 '18 at 01:23
  • i am updating an example... please wait – toto Feb 16 '18 at 01:26
  • 1
    **Important note:** This should be only used as a workaround. It will send 30% more data and will also require more work server side. When available (99% of the time) send files as multipart (binary). – Kaiido Feb 16 '18 at 01:47
  • Also why are you doing the AB to b64 conversion yourself? FileReader as a `readAsDataURL` method which will make this way faster than your own implementation. All you'd have to do then is `data = reader.result.split(',')[1]` – Kaiido Feb 16 '18 at 01:55
  • Yes Kaido have a reason about the method. I will update the code. – toto Feb 16 '18 at 02:31
  • 1
    Hello Hassan, I update the code. Now using: fl.readAsBinaryString(selectedFile); this is the real file length. From your side Python you need save Binary String to File. May this link help you: https://www.devdungeon.com/content/working-binary-data-python – toto Feb 16 '18 at 02:38
  • Ouch no. Don't use readAsBinaryString actually don't ever do it, it's been deprecated. This will return an utf16 version of the binary, and there are great chances some charactersbwon't pass through one or the other hand. base64 is unfortunately the best way to send a string version of binary data, but FileReader has a built in readAsDataURL method. – Kaiido Feb 16 '18 at 09:00