JavaScript Blob to download a binary file creating corrupted files
Asked Answered
C

1

1

I have a binary file (python pickle file, to be exact). Whenever such a file is requested, I create one on server side, and then send it to the client via flask's send_file as an AJAX request.

Next, I need to download this file automatically to the client side, so I have used this answer.

The problem is that, the created file on the server normally has a size 300 Bytes, and the file downloaded on the client side is of the size >500 Bytes. Plus whenever I try to reuse the pickle file, it doesn't load, giving the error:

_pickle.UnpicklingError: invalid load key, '\xef'.

Whereas, the server file is loaded seamlessly. So, the problem is, the client side file is corrupted, while in transmission. I think the js blob might be the culprit.

Has anyone seen something like this before?


Server side code handling the AJAX (flask)

@app.route("/_exportTest",methods=['POST'])
def exportTest():
    index = int(request.form['index'])
    path = g.controller.exportTest(testID=index)
    logger.debug("Test file path :"+path)
    return send_file(path) #this is wrong somehow

Regarding the exportTest function:

def exportTest(self,testName):
    dic = dict() 
    dic['screenShot'] = self.screenShot #string
    dic['original_activity'] = self.original_activity #string
    dic['steps'] = self.steps #list of tuples of strings
    if self.exportFilePath=='.': #this is the case which will be true
        filePath = os.path.join(os.getcwd(),testName) 
    else:
        filePath = os.path.join(os.getcwd(),self.exportFilePath,testName)
    logger.debug("filePath :"+filePath)
    try:
        pickle.dump(dic,open(filePath,"wb"),protocol=pickle.HIGHEST_PROTOCOL)
    except Exception as e:
        logger.debug("Error while pickling Test.\n Error :"+str(e)) #No such error was printed
    return filePath

Client side code:

$.ajax({

            type: "POST",
            // url: "/_exportTest",
            url:"/_exportTest",
            data:{index:testIndex},
            success: function(response, status, xhr) {
                // check for a filename
                var filename = "TEST_"+testIndex+".tst";
                var disposition = xhr.getResponseHeader('Content-Disposition');
                if (disposition && disposition.indexOf('attachment') !== -1) {
                    var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
                    var matches = filenameRegex.exec(disposition);
                    if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
                }
                
                var type = xhr.getResponseHeader('Content-Type');
                var blob = new Blob([response],{type:type});//, { type: type });
                console.log("Binary type :"+type) ;
                if (typeof window.navigator.msSaveBlob !== 'undefined') {
                    // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
                    console.log("WINDOW NAVIGATION MSSAVEBLOB type if undefined") ;
                    window.navigator.msSaveBlob(blob, filename);
                } 
                else {
                    console.log("ELSE1")
                    var URL = window.URL || window.webkitURL;
                    var downloadUrl = URL.createObjectURL(blob);

                    if (filename) {
                        console.log("Filename exists") ;
                        // use HTML5 a[download] attribute to specify filename
                        var a = document.createElement("a");
                        // safari doesn't support this yet
                        if (typeof a.download === 'undefined') {
                            console.log("typeof a.download is undefined") ;
                            window.location.href = downloadUrl;
                        } else {
                            console.log("typeof a.download is not undefined") ;
                            a.href = downloadUrl;
                            a.download = filename;
                            document.body.appendChild(a);
                            a.click();
                        }
                    } else {
                        console.log("Filename does not exist") ;
                        window.location.href = downloadUrl;
                    }
                    // window.location.href = downloadUrl;
                    setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
                }
            }
        });
Credo answered 6/10, 2020 at 7:11 Comment(8)
you may be doing something wrong in your code (either client or server)Adjoint
So this is the correct way to do this, right? There is nothing special to be done when handling binary files?Credo
this is the correct way - not sure what this is, as you've shown no codeAdjoint
I have copied the blob code from the answer I've linked. If any other code snippets are required, I will attach them. Do let me know.Credo
which one? there's two versions of code in that one answer, there's a more modern version in a later answer, and there's absolutely nothing shown about the server sideAdjoint
The newer one. Server side code attached.Credo
On the client side, are you using XMLHttpRequest, jQuery.ajax, or some other wrapper? Also try sending a mime type on the server side: send_file(path, 'application/octet-stream').Buster
@JonathanAmend I am using jQuery.ajax. I tried this, but got the same result. Curiously, in order to isolate the problem, I had created another route _testURL just for this, which simply did send_file(<file-path>). Now, when I tried to do a get then it downloaded the correct file. But when I used your answer's code, it again increased size. So, could it be possible that there is something mangling with the byte string?Credo
C
4

Weirdly enough, I was looking into this answer, which worked. So, I added :

xhrFields: {
    responseType:'blob'
},

in the AJAX request, which solved the problem for me.

I have absolutely no idea, why this worked, so can someone give a better answer than this?


At MDN Docs:

The values supported by responseType are the following:
An empty responseType string is treated the same as "text", the default type.
arraybuffer
The response is a JavaScript ArrayBuffer containing binary data.
blob
The response is a Blob object containing the binary data.
...
Credo answered 7/10, 2020 at 6:24 Comment(2)
It looks like you based your client-side code off the old solution in my answer, which was using jQuery.ajax. The problem with it is specifically what you were seeing, which was jQuery mangling the binary data. At that time, the xhrFields option did not exist. The better option was to use XMLHTTPRequest directly and set xhr.responseType = 'arraybuffer', which like using blob as you did, would avoid mangling the binary response.Buster
@JonathanAmend I changed the code, and replaced with xhr.responseType and it works wonderfully now!Credo

© 2022 - 2024 — McMap. All rights reserved.