Prompt file download with XMLHttpRequest
Asked Answered
O

4

44

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.

Occupier answered 28/3, 2014 at 22:12 Comment(3)
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?Circosta
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?Occupier
Can you unaccept my answer and accept Steven's one instead? Having the outdated one on top is really confusingCircosta
C
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

Circosta answered 30/3, 2014 at 0:16 Comment(5)
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.Occupier
how about if he is using POST request but need to donwnload afterwards?Shriek
@Shriek 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)Circosta
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 tabQuarterphase
Here is an interesting hack that works pretty well:Exploration
D
62

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'));
}
Dorsman answered 8/6, 2017 at 12:17 Comment(7)
this is a pretty interesting solution. I'm trying something similar but in WebView container - see here #49988702Kris
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).Apery
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.Haroldharolda
It seems as though the 'match' is returning a filename with underscores (when the content-disposition filename contains surrounding quotation)Tavie
@Tavie you can use fileName = fileName.slice(1, -1); inside the saveBlob function to avoid the underscores.Wiggs
Note the saveOrOpenBlob function uses requestFileSystem, which is not recommended per MDN: developer.mozilla.org/en-US/docs/Web/API/Window/…Redhot
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" lineSidelong
C
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

Circosta answered 30/3, 2014 at 0:16 Comment(5)
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.Occupier
how about if he is using POST request but need to donwnload afterwards?Shriek
@Shriek 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)Circosta
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 tabQuarterphase
Here is an interesting hack that works pretty well:Exploration
T
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.

Twin answered 10/10, 2018 at 11:12 Comment(1)
Doubt you need to append the tempEl to the document.body, but this should work fine. I like the revokeObjectURL too.Occupier
E
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);
        });
};
Ekaterinburg answered 11/8, 2023 at 6:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.