42

I'm aware that jQuery's ajax method cannot handle downloads, and I do not want to add a jQuery plugin to do this.

I want to know how to send POST data with XMLHttpRequest to download a file.

Here's what I've tried:

var postData = new FormData();
postData.append('cells', JSON.stringify(output));

var xhr = new XMLHttpRequest();
xhr.open('POST', '/export/', true);
xhr.setRequestHeader("X-CSRFToken", csrftoken);
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
    console.log(e);
    console.log(xhr);
}
xhr.send(postData);

I'm working with Django, and the file appears to be sending back to the client successfully. In the network tab in Chrome, I can see gibberish in the preview tab (which I expect). But I want to send back a zip file, not a text representation of the zip file. Here's the Django back end:

wrapper = FileWrapper(tmp_file)
response = HttpResponse(wrapper, content_type='application/zip')
response['Content-Disposition'] = "attachment; filename=export.zip"
response['Content-Length'] = tmp_file.tell()
return response

I've searched this for hours now without finding a proper example on how to do this with XMLHttpRequests. I don't want to create a proper html form with a POST action because the form data is rather large, and dynamically created.

Is there something wrong with the above code? Something I'm missing? I just don't know how to actually send the data to the client as a download.

bozdoz
  • 12,550
  • 7
  • 67
  • 96
  • I don't see anything wrong here. If you change view to return file in response to GET request and open the URL in browser, does it download file as expected? – Marat Mar 29 '14 at 13:29
  • Yeah, it downloads @Marat. I was able to get it to download by just using a normal html form with an action to '/export/', and I get a response from the xhr, but it just doesn't trigger a download. Is it possible that xhr can't download to the client? – bozdoz Mar 29 '14 at 19:50
  • 1
    Can you unaccept my answer and accept Steven's one instead? Having the outdated one on top is really confusing – Marat May 05 '21 at 14:28

4 Answers4

60

If you set the XMLHttpRequest.responseType property to 'blob' before sending the request, then when you get the response back, it will be represented as a blob. You can then save the blob to a temporary file and navigate to it.

var postData = new FormData();
postData.append('cells', JSON.stringify(output));

var xhr = new XMLHttpRequest();
xhr.open('POST', '/export/', true);
xhr.setRequestHeader('X-CSRFToken', csrftoken);
xhr.responseType = 'blob';
xhr.onload = function (e) {
    var blob = e.currentTarget.response;
    var contentDispo = e.currentTarget.getResponseHeader('Content-Disposition');
    // https://stackoverflow.com/a/23054920/
    var fileName = contentDispo.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1];
    saveOrOpenBlob(blob, fileName);
}
xhr.send(postData);

And here's an example implementation of saveOrOpenBlob:

function saveOrOpenBlob(blob, fileName) {
    window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
    window.requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fs) {
        fs.root.getFile(fileName, { create: true }, function (fileEntry) {
            fileEntry.createWriter(function (fileWriter) {
                fileWriter.addEventListener("writeend", function () {
                    window.location = fileEntry.toURL();
                }, false);
                fileWriter.write(blob, "_blank");
            }, function () { });
        }, function () { });
    }, function () { });
}

If you don't care about having the browser navigate to the file when it's a viewable file type, then making a method that always saves directly to file is much simpler:

function saveBlob(blob, fileName) {
    var a = document.createElement('a');
    a.href = window.URL.createObjectURL(blob);
    a.download = fileName;
    a.dispatchEvent(new MouseEvent('click'));
}
HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
Steven Doggart
  • 43,358
  • 8
  • 68
  • 105
  • this is a pretty interesting solution. I'm trying something similar but in `WebView` container - see here https://stackoverflow.com/questions/49988202/macos-webview-download-a-html5-blob-file – loretoparisi Apr 24 '18 at 07:25
  • 12
    This is a good solution for small files, but notice that this puts the whole file in memory before sending it to disk, so take that into account if downloading very large files. Also the user won't be able to see the progress of the actual download (like in Chrome's statusbar), what they'll see is the progress of the "download" from memory to disk instead (once the actual download has finished). – Daniel Jun 20 '18 at 23:03
  • After having integrated your solution in my project, I took the liberty to fix a couple of things in your otherwise most excellent answer. Please let me know if I introduced any error. – Arnaud P Dec 27 '18 at 09:01
  • 1
    It seems as though the 'match' is returning a filename with underscores (when the content-disposition filename contains surrounding quotation) – sookie Feb 25 '19 at 18:09
  • @sookie you can use fileName = fileName.slice(1, -1); inside the saveBlob function to avoid the underscores. – the_ccalderon Nov 26 '19 at 16:22
  • 2
    Note the saveOrOpenBlob function uses requestFileSystem, which is not recommended per MDN: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestFileSystem – HappyGoLucky Dec 16 '20 at 17:55
  • Looks interesting, but what about on server? I currently have "content-type" set to "application/zip" before adding the "content-disposition" header. (When called via HTML link) Would I still use this arrangement with HTTPRequest? And just to confuse things, how would you 'trap' a text message, (ie "The file not found" being returned to print to innerHTML rather than opening a save dialogue?) Got a feeling it will be a 'mod' of the "contentDispo" line – Cristofayre Feb 24 '21 at 11:05
22

UPDATE: this answer is not accurate anymore since the introduction of Blob API. Please refer to Steven's answer for details.


ORIGINAL ANSWER:

XHR request will not trigger file download. I can't find explicit requirement, but W3C doc on XMLHttpRequest doesn't describe any special reaction on content-disposition=attachment responses either

You could download file by window.open() in separate tab, if it was not POST request. Here it was suggested to use a hidden form with target=_blank

Marat
  • 15,215
  • 2
  • 39
  • 48
  • Not sure if you actually need window.open though. I've accomplished what I set out to do without `target="_blank"`. But I suppose that this is correct that XHR doesn't trigger downloads. – bozdoz Mar 30 '14 at 01:04
  • how about if he is using POST request but need to donwnload afterwards? – gumuruh Sep 25 '19 at 14:16
  • 1
    @gumuruh I'm not sure what is the difference with the original question. Also, please note that this answer is not factually true anymore (see the update part) – Marat Sep 25 '19 at 14:50
  • Opening in a new tab would make the client download the file twice (unless cached), which is a consideration for large files or mobile connections. Once to check if it's a download, and twice when loaded in a new tab – Matthias Oct 09 '19 at 19:01
  • [Here is](https://www.alexhadik.com/writing/xhr-file-download/) an interesting hack that works pretty well: – James Toomey Jun 05 '20 at 23:09
4

download: function(){
    var postData = new FormData();
  var xhr = new XMLHttpRequest();
  xhr.open('GET', downloadUrl, true);
  xhr.responseType = 'blob';
  xhr.onload = function (e) {
   var blob = xhr.response;
   this.saveOrOpenBlob(blob);
  }.bind(this)
  xhr.send(postData);
 }

saveOrOpenBlob: function(blob) {
  var assetRecord = this.getAssetRecord();
  var fileName = 'Test.mp4'
  var tempEl = document.createElement("a");
     document.body.appendChild(tempEl);
     tempEl.style = "display: none";
        url = window.URL.createObjectURL(blob);
        tempEl.href = url;
        tempEl.download = fileName;
        tempEl.click();
  window.URL.revokeObjectURL(url);
 },

Try this it is working for me.

  • Doubt you need to append the tempEl to the document.body, but this should work fine. I like the `revokeObjectURL` too. – bozdoz Jan 23 '20 at 14:35
0

For me it worked with fetch API. I couldn't make it work with XMLHttpRequest in React/Typescript project. Maybe I didn't try enough. Anyway, problem is solved. Here is code:

const handleDownload = () => {
    const url = `${config.API_ROOT}/download_route/${entityId}`;
    const headers = new Headers();
    headers.append('Authorization', `Token ${token}`);

    fetch(url, { headers })
        .then((response) => response.blob())
        .then((blob) => {
            // Create  blob  URL
            const blobUrl = window.URL.createObjectURL(blob);

            // Create a temporary anchor el
            const anchorEl = document.createElement('a');
            anchorEl.href = blobUrl;
            anchorEl.download = `case_${entityId}_history.pdf`; // Set any filename and extension
            anchorEl.style.display = 'none';

            // Append the a tag to the DOM and click it to trigger download
            document.body.appendChild(anchorEl);
            anchorEl.click();

            // Clean up
            document.body.removeChild(anchorEl);
            window.URL.revokeObjectURL(blobUrl);
        })
        .catch((error) => {
            console.error('Error downloading file:', error);
        });
};
parnas
  • 21
  • 7