117

I want to send an "ajax download request" when I click on a button, so I tried in this way:

javascript:

var xhr = new XMLHttpRequest();
xhr.open("GET", "download.php");
xhr.send();

download.php:

<?
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename= file.txt");
header("Content-Transfer-Encoding: binary");    
readfile("file.txt");
?>

but doesn't work as expected, how can I do ? Thank you in advance

Manuel Di Iorio
  • 3,631
  • 5
  • 29
  • 31

13 Answers13

104

Update April 27, 2015

Up and coming to the HTML5 scene is the download attribute. It's supported in Firefox and Chrome, and soon to come to IE11. Depending on your needs, you could use it instead of an AJAX request (or using window.location) so long as the file you want to download is on the same origin as your site.

You could always make the AJAX request/window.location a fallback by using some JavaScript to test if download is supported and if not, switching it to call window.location.

Original answer

You can't have an AJAX request open the download prompt since you physically have to navigate to the file to prompt for download. Instead, you could use a success function to navigate to download.php. This will open the download prompt but won't change the current page.

$.ajax({
    url: 'download.php',
    type: 'POST',
    success: function() {
        window.location = 'download.php';
    }
});

Even though this answers the question, it's better to just use window.location and avoid the AJAX request entirely.

Community
  • 1
  • 1
Steven Lambert
  • 5,571
  • 2
  • 29
  • 46
  • As @ManuelDiIorio said, a simple `window.location` resolves the question. So I think the reply from **Jelle Kralt** below answers better the question. – Gustavo Straube Oct 20 '14 at 14:52
  • 45
    Doesn't this call the link twice? I'm in a similar boat... I'm passing a lot of security information in headers, and able to parse the file object in the success function, but don't know how to trigger a download prompt. – user1447679 Apr 30 '15 at 01:13
  • 4
    It does call the page twice, so if you are querying a database in that page, this means 2 trips to DB. – mmmmmm Apr 05 '16 at 07:43
  • 1
    @user1447679 see for an alternative solution: http://stackoverflow.com/questions/38665947/in-javascript-how-can-i-redirect-an-ajax-response-in-window-location – John Jul 29 '16 at 20:39
  • 2
    Let me explain how this helped me... the example could have been more complete. with "download.php?get_file=true" or something... I have an ajax function that does some error checking on a form submission and then creates a csv file. If the error check fails, it has to come back with why it failed. If it creates the CSV it is telling the parent that "go ahead and fetch the file". I do that by posting to the ajax file with the form variable then posting a DIFFERENT parameter to the same file saying "hand me the file you just created" (path/name is hard coded into the ajax file). – Scott Jun 30 '17 at 17:05
  • 8
    But it will send request 2 times, that is not proper – Dharmendrasinh Chudasama Feb 17 '18 at 13:07
  • So I see some comments where there is complaint that this is not useful but that is not always the case. Consider an S3 presigned url with customer provided keys on encryption. You still have to provide custom headers on the get to download the object so simply setting window.locaion = signedurl will not work you need to send the encryption key and other info in custom AWS specific headers so this is worthy of an upvote. – Steve Owens Aug 22 '18 at 17:48
  • 1
    This is a bad idea. it call the url twice. It makes no sense at all. Why dont you execulte window.location = 'download.php' without the ajax overhead? – Gonzalo De-Spirito Jul 31 '19 at 19:15
  • Several years later and its still not supported in IE11, sad times. – Hughsie28 Oct 11 '21 at 10:27
61

To make the browser downloads a file you need to make the request like that:

 function downloadFile(urlToSend) {
     var req = new XMLHttpRequest();
     req.open("GET", urlToSend, true);
     req.responseType = "blob";
     req.onload = function (event) {
         var blob = req.response;
         var fileName = req.getResponseHeader("fileName") //if you have the fileName header available
         var link=document.createElement('a');
         link.href=window.URL.createObjectURL(blob);
         link.download=fileName;
         link.click();
     };

     req.send();
 }
João Marcos
  • 3,872
  • 1
  • 19
  • 14
48

You actually don't need ajax at all for this. If you just set "download.php" as the href on the button, or, if it's not a link use:

window.location = 'download.php';

The browser should recognise the binary download and not load the actual page but just serve the file as a download.

Jelle Kralt
  • 980
  • 6
  • 16
  • 3
    The programming language you're using to change `window.location` *is* JavaScript. – mikemaccana Apr 22 '14 at 18:50
  • 2
    You're right @mikemaccana, I actually meant ajax :). – Jelle Kralt Apr 23 '14 at 07:26
  • 2
    Have been hunting high and low for a solution and this is so elegant and perfect. Thank you so much. – Yangshun Tay May 16 '16 at 09:27
  • 2
    Of course, this solution will only work if it is a static file that already exists. – krillgar Sep 14 '17 at 19:25
  • 2
    If the server responds with an error though there won't be any way to stay on your main page without being redirected to an error page by the browser. At least this is what Chrome does when the result of window.location returns 404. – Ian May 30 '18 at 16:51
  • wow I overthought it. I just added my file name as a GET param and bingo. Thanks! – CJ Broersma Jul 20 '21 at 19:35
22

Cross browser solution, tested on Chrome, Firefox, Edge, IE11.

In the DOM, add an hidden link tag:

<a id="target" style="display: none"></a>

Then:

var req = new XMLHttpRequest();
req.open("GET", downloadUrl, true);
req.responseType = "blob";
req.setRequestHeader('my-custom-header', 'custom-value'); // adding some headers (if needed)

req.onload = function (event) {
  var blob = req.response;
  var fileName = null;
  var contentType = req.getResponseHeader("content-type");

  // IE/EDGE seems not returning some response header
  if (req.getResponseHeader("content-disposition")) {
    var contentDisposition = req.getResponseHeader("content-disposition");
    fileName = contentDisposition.substring(contentDisposition.indexOf("=")+1);
  } else {
    fileName = "unnamed." + contentType.substring(contentType.indexOf("/")+1);
  }

  if (window.navigator.msSaveOrOpenBlob) {
    // Internet Explorer
    window.navigator.msSaveOrOpenBlob(new Blob([blob], {type: contentType}), fileName);
  } else {
    var el = document.getElementById("target");
    el.href = window.URL.createObjectURL(blob);
    el.download = fileName;
    el.click();
  }
};
req.send();
leo
  • 1,243
  • 1
  • 18
  • 20
15

It is possible. You can have the download started from inside an ajax function, for example, just after the .csv file is created.

I have an ajax function that exports a database of contacts to a .csv file, and just after it finishes, it automatically starts the .csv file download. So, after I get the responseText and everything is Ok, I redirect browser like this:

window.location="download.php?filename=export.csv";

My download.php file looks like this:

<?php

    $file = $_GET['filename'];

    header("Cache-Control: public");
    header("Content-Description: File Transfer");
    header("Content-Disposition: attachment; filename=".$file."");
    header("Content-Transfer-Encoding: binary");
    header("Content-Type: binary/octet-stream");
    readfile($file);

?>

There is no page refresh whatsoever and the file automatically starts downloading.

NOTE - Tested in the following browsers:

Chrome v37.0.2062.120 
Firefox v32.0.1
Opera v12.17
Internet Explorer v11
Pedro Sousa
  • 445
  • 6
  • 12
  • 1
    Isn't this dangerous security-wise? – Mickael Bergeron Néron Jun 22 '16 at 17:32
  • @MickaelBergeronNéron Why? – Pedro Sousa Jun 23 '16 at 08:12
  • 5
    I would think so because anybody can call download.php?filename=[something] and try some path and file names, especially common ones, and this could even be done inside a loop within a program or a script. – Mickael Bergeron Néron Jun 23 '16 at 12:43
  • wouldn't .htaccess avoid that? – Pedro Sousa Jun 23 '16 at 16:22
  • 9
    @PedroSousa .. no. htaccess controls access to the file structure via Apache. Since the access has reached a PHP script, htaccess now stops its duty. This IS VERY MUCH a security hole because indeed, any file that PHP (and the user it is being run under) can read, so it can deliver into the readfile... One should always sanitise the requested file to be read – Prof Jul 26 '16 at 20:56
  • @prof83 I guess you are right. For the sake of security, we should always sanitise files to be requested. Thanks for poiting it out, i will definitely have that into account in the future. Thanks :) – Pedro Sousa Jul 28 '16 at 12:46
  • Downloading period is dangerous. Using a token system or some sort of download management is required for all answers here to keep them safe. These worries are valid for every one of them that doesn't have the security/management. – JSG Jun 28 '18 at 05:13
4

I prefer location.assign(url);

Complete syntax example:

document.location.assign('https://www.urltodocument.com/document.pdf');

developer.mozilla.org/en-US/docs/Web/API/Location.assign

Telmo Dias
  • 3,938
  • 2
  • 36
  • 48
4

For those looking a more modern approach, you can use the fetch API. The following example shows how to download a spreadsheet file. It is easily done with the following code.

fetch(url, {
    body: JSON.stringify(data),
    method: 'POST',
    headers: {
        'Content-Type': 'application/json; charset=utf-8'
    },
})
.then(response => response.blob())
.then(response => {
    const blob = new Blob([response], {type: 'application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
    const downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = downloadUrl;
    a.download = "file.xlsx";
    document.body.appendChild(a);
    a.click();
})

I believe this approach to be much easier to understand than other XMLHttpRequest solutions. Also, it has a similar syntax to the jQuery approach, without the need to add any additional libraries.

Of course, I would advise checking to which browser you are developing, since this new approach won't work on IE. You can find the full browser compatibility list on the following link.

Important: In this example I am sending a JSON request to a server listening on the given url. This url must be set, on my example I am assuming you know this part. Also, consider the headers needed for your request to work. Since I am sending a JSON, I must add the Content-Type header and set it to application/json; charset=utf-8, as to let the server know the type of request it will receive.

Alain Cruz
  • 4,757
  • 3
  • 25
  • 43
2

@Joao Marcos solution works for me but I had to modify the code to make it work on IE, below if what the code looks like

       downloadFile(url,filename) {
        var that = this;
        const extension =  url.split('/').pop().split('?')[0].split('.').pop();

        var req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.responseType = "blob";
        req.onload = function (event) {
            const fileName = `${filename}.${extension}`;
            const blob = req.response;

            if (window.navigator.msSaveBlob) { // IE
                window.navigator.msSaveOrOpenBlob(blob, fileName);
            } 
            const link = document.createElement('a');
            link.href = window.URL.createObjectURL(blob);                
            link.download = fileName;
            link.click();
            URL.revokeObjectURL(link.href);

        };

        req.send();
    },
Jemil Oyebisi
  • 633
  • 8
  • 10
0

Decoding a filename from the header is a little bit more complex...

    var filename = "default.pdf";
    var disposition = req.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, '');
    }
Jaime
  • 5,770
  • 4
  • 23
  • 50
Lumic
  • 61
  • 2
  • 3
    Please format your entire code block and provide some additional explanation to your process for future reader benefit. – mickmackusa Mar 20 '17 at 23:49
0

This solution is not very different from those above, but for me it works very well and i think it's clean.

I suggest to base64 encode the file server side (base64_encode(), if you are using PHP) and send the base64 encoded data to the client

On the client you do this:

 let blob = this.dataURItoBlob(THE_MIME_TYPE + "," + response.file);
 let uri = URL.createObjectURL(blob);
 let link = document.createElement("a");
 link.download = THE_FILE_NAME,
 link.href = uri;
 document.body.appendChild(link);
 link.click();
 document.body.removeChild(link);

This code puts the encoded data in a link and simulates a click on the link, then it removes it.

martinethyl
  • 184
  • 1
  • 12
  • You can just make the a tag hidden and populate the href dynamically. no need to add and remove – BPeela Feb 01 '19 at 18:29
0

Your needs are covered by window.location('download.php');
But I think that you need to pass the file to be downloaded, not always download the same file, and that's why you are using a request, one option is to create a php file as simple as showfile.php and do a request like

var myfile = filetodownload.txt
var url = "shofile.php?file=" + myfile ;
ajaxRequest.open("GET", url, true);

showfile.php

<?php
$file = $_GET["file"] 
echo $file;

where file is the file name passed via Get or Post in the request and then catch the response in a function simply

if(ajaxRequest.readyState == 4){
                        var file = ajaxRequest.responseText;
                        window.location = 'downfile.php?file=' + file;  
                    }
                }
lisandro
  • 454
  • 4
  • 12
0

there is another solution to download a web page in ajax. But I am referring to a page that must first be processed and then downloaded.

First you need to separate the page processing from the results download.

1) Only the page calculations are made in the ajax call.

$.post("CalculusPage.php", { calculusFunction: true, ID: 29, data1: "a", data2: "b" },

       function(data, status) 
       {
            if (status == "success") 
            {
                /* 2) In the answer the page that uses the previous calculations is downloaded. For example, this can be a page that prints the results of a table calculated in the ajax call. */
                window.location.href = DownloadPage.php+"?ID="+29;
            }               
       }
);

// For example: in the CalculusPage.php

    if ( !empty($_POST["calculusFunction"]) ) 
    {
        $ID = $_POST["ID"];

        $query = "INSERT INTO ExamplePage (data1, data2) VALUES ('".$_POST["data1"]."', '".$_POST["data2"]."') WHERE id = ".$ID;
        ...
    }

// For example: in the DownloadPage.php

    $ID = $_GET["ID"];

    $sede = "SELECT * FROM ExamplePage WHERE id = ".$ID;
    ...

    $filename="Export_Data.xls";
    header("Content-Type: application/vnd.ms-excel");
    header("Content-Disposition: inline; filename=$filename");

    ...

I hope this solution can be useful for many, as it was for me.

netluke
  • 51
  • 2
0

this works for me

var dataObj = {
somekey:"someValue"
}
     $.ajax({
        method: "POST",
        url: "/someController/someMethod",
        data: dataObj,
        success: function (response) {
            const blob = new Blob([response], { type: 'text/csv' });
            const downloadUrl = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.href = downloadUrl;
            a.download = "file.csv";
            document.body.appendChild(a);
            a.click();
        }
    });
Ryan Dooley
  • 224
  • 1
  • 3
  • 16