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
                }
            }
        });
Mooncrater
  • 4,146
  • 4
  • 33
  • 62
  • you may be doing something wrong in your code (either client or server) – Jaromanda X Oct 06 '20 at 07:15
  • So this is the correct way to do this, right? There is nothing special to be done when handling binary files? – Mooncrater Oct 06 '20 at 07:33
  • `this is the correct way` - not sure what `this` is, as you've shown no code – Jaromanda X Oct 06 '20 at 07:34
  • 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. – Mooncrater Oct 06 '20 at 07:35
  • 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 side – Jaromanda X Oct 06 '20 at 07:36
  • The newer one. Server side code attached. – Mooncrater Oct 06 '20 at 07:37
  • 1
    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')`. – Jonathan Amend Oct 06 '20 at 17:02
  • @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()`. 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? – Mooncrater Oct 07 '20 at 05:52

1 Answers1

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.
...
Mooncrater
  • 4,146
  • 4
  • 33
  • 62
  • 1
    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. – Jonathan Amend Oct 07 '20 at 16:35
  • @JonathanAmend I changed the code, and replaced with `xhr.responseType` and it works wonderfully now! – Mooncrater Oct 09 '20 at 05:42