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.

7
  • I recomended you use a "FileReader" and send data to server as binary or base64 Commented Feb 15, 2018 at 5:33
  • i assume that you need upload file using File object ? the user select the file first and send it... rigth? Commented Feb 15, 2018 at 23:41
  • @toto: correct. In fact, it's blob object (file object is an extension of blob). Commented Feb 15, 2018 at 23:59
  • in Your server side you are using php, jsp ? Commented Feb 16, 2018 at 0:15
  • @toto: Well I'm using Python (django framework), but the concepts ought to be the same Commented Feb 16, 2018 at 0:31

2 Answers 2

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.
Sign up to request clarification or add additional context in comments.

4 Comments

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.
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.
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.
Yea, you're right, that's one way to do it. Thanks.
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>

8 Comments

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?
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.
i am updating an example... please wait
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).
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: devdungeon.com/content/working-binary-data-python
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.