447

I have a Struts2 action in the server side for file downloading.

<action name="download" class="com.xxx.DownAction">
    <result name="success" type="stream">
        <param name="contentType">text/plain</param>
        <param name="inputName">imageStream</param>
        <param name="contentDisposition">attachment;filename={fileName}</param>
        <param name="bufferSize">1024</param>
    </result>
</action>

However when I call the action using the jQuery:

$.post(
  "/download.action",{
    para1:value1,
    para2:value2
    ....
  },function(data){
      console.info(data);
   }
);

in Firebug I see the data is retrieved with the Binary stream. I wonder how to open the file downloading window with which the user can save the file locally?

Cœur
  • 32,421
  • 21
  • 173
  • 232
hguser
  • 31,173
  • 49
  • 145
  • 269
  • 1
    possible duplicate of [How to download a file on clicking the name of file using PHP?](http://stackoverflow.com/questions/4518702/how-to-download-a-file-on-clicking-the-name-of-file-using-php) – Pekka Dec 28 '10 at 10:29
  • 1
    I marked it as a duplicate despite the platform difference, because as far as I can see the solution is the same (You can't and don't need to do this through Ajax). – Pekka Dec 28 '10 at 10:30
  • 1
    so,without ajax,just use the window.location="download.action?para1=value1...."? – hguser Dec 28 '10 at 10:32

25 Answers25

721

2019 modern browsers update

This is the approach I'd now recommend with a few caveats:

  • A relatively modern browser is required
  • If the file is expected to be very large you should likely do something similar to the original approach (iframe and cookie) because some of the below operations could likely consume system memory at least as large as the file being downloaded and/or other interesting CPU side effects.

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(resp => resp.blob())
  .then(blob => {
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = url;
    // the filename you want
    a.download = 'todo-1.json';
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    alert('your file has downloaded!'); // or you know, something with better UX...
  })
  .catch(() => alert('oh no!'));

2012 Original jQuery/iframe/Cookie based approach

Bluish is completely right about this, you can't do it through Ajax because JavaScript cannot save files directly to a user's computer (out of security concerns). Unfortunately pointing the main window's URL at your file download means you have little control over what the user experience is when a file download occurs.

I created jQuery File Download which allows for an "Ajax like" experience with file downloads complete with OnSuccess and OnFailure callbacks to provide for a better user experience. Take a look at my blog post on the common problem that the plugin solves and some ways to use it and also a demo of jQuery File Download in action. Here is the source

Here is a simple use case demo using the plugin source with promises. The demo page includes many other, 'better UX' examples as well.

$.fileDownload('some/file.pdf')
    .done(function () { alert('File download a success!'); })
    .fail(function () { alert('File download failed!'); });

Depending on what browsers you need to support you may be able to use https://github.com/eligrey/FileSaver.js/ which allows more explicit control than the IFRAME method jQuery File Download uses.

John Culviner
  • 19,634
  • 5
  • 47
  • 47
  • 71
    I love what you built but I suspect that to get more StackOverFlow credit your answer here should contain a bit more detail. Specifically on how you solved the problem. – AnthonyVO Aug 07 '12 at 21:58
  • can this work to force the browser to show a save dialog/ribbon for .png files as well? – Pierpaolo May 15 '13 at 11:59
  • 14
    it would be nice if you would mention exactly how this "plugin" gets around the limitation, rather than forcing us to go to your blog/plugin source to see it. for example, is it instead posting to an iframe? is it instead requiring the remote script to save the file and return a url to it? – Kevin B Sep 23 '13 at 19:47
  • @KevinB the link in the answer explains it all – asgerhallas Oct 29 '13 at 10:45
  • 2
    @asgerhallas Sure, but that's completely useless if said link goes away. – Kevin B Oct 29 '13 at 14:06
  • Can I use $.fileDownload with POST instead of GET request? – Adrian Oct 30 '13 at 13:34
  • 26
    I agree, a blog is a far better place to place a lengthy description of how to use your plugin and how it works. but you could have at least gave a short overview of how this plugin solves the problem. For example, this solves the problem by having the server set a cookie and having your javascript continuously look for the cookie until it exists. Once it exists, we can assume that the download is complete. With that kind of information one could easily roll their own solution very quickly, and the answer no longer relies 100% on your blog/plugin/jquery and can be applied to other libraries. – Kevin B Oct 30 '13 at 20:10
  • great script , however it would've been better to check xmkHttpRequest support for file download , and then fallback to iframes. – Royi Namir Jan 12 '14 at 08:35
  • 1
    Royi, as I understand it AJAX can *never* support file downloads that result in a file download popup to save to disk. Have you found a way that I'm unaware of? – John Culviner Jan 13 '14 at 16:19
  • If I want to download an image from an API like: https://api.qrserver.com/v1/create-qr-code/?data=hello+world, It gets failed and gives this message: Resource interpreted as Document but transferred with MIME type image/png – osmanz Apr 29 '14 at 08:41
  • why I get ** $(...).html(...).dialog is not a function** error in line 85 of js? should I add sth more than the js file ? – Sara N Dec 09 '15 at 05:04
  • I don't understand : where does it says that cookie is written after the file has been downloaded. this is not a deterministic order. – Royi Namir Aug 22 '16 at 17:10
  • Well... I really don't want to underrate your answer - it works - yes, but this answer and solution is overblown imho. This solution uses an iFrame - a technique that is known and used for years. Nothing special. A better answer would explain how it works and what to do: No jQuery needed. Just create an iFrame element and set `.src` or hidden fields and then `.submit()` to allow POST requests. If only a GET request with few and short params required, not even an iFrame is needed. Just change current location by `window.location.href`. That jQuery wrapper could be added to the answer then. – StanE Sep 25 '16 at 19:27
  • 1
    @StanE thanks for the comment. I updated the bottom of my answer which clarifies some better options now that it is 2016 (and jQuery was almost a given back when I wrote this plugin and answered this question -- which is certainly not the case anymore) – John Culviner Sep 27 '16 at 16:28
  • @JohnCulviner your blog link is being blocked by eset antivirus, it suggests your page contains malware so it prints "blocked by internal blacklist", just fyi – PersyJack Feb 05 '19 at 18:53
  • Given that Luke Madhanga's answer shows that using HTML5 you can do it using AJAX, could you edit out the "you can't do it through Ajax because JavaScript cannot save files directly to a user's computer (out of security concerns)" line at the top of your answer? Your answer has so many more upvotes that it initially threw me off thinking it's still not possible. Not everyone might scroll down to read all the other answers. – Matthijs Wessels Feb 21 '19 at 10:18
  • @JohnCulviner Really nice answer, about the 2019 update: could you precise what you mean by **very large** files? Are we talking MB, GB? What's the limitation exactly? Cheers – PaulCo May 07 '20 at 13:50
  • Anything that would use up a signifigant amount of RAM on the device that's downloading it. – John Culviner May 07 '20 at 18:09
  • Very useful, thank you! Maybe the `window.URL.revokeObjectURL(url);` should be in a finally block? – EM0 Apr 26 '21 at 12:26
237

Noone posted this @Pekka's solution... so I'll post it. It can help someone.

You don't need to do this through Ajax. Just use

window.location="download.action?para1=value1...."
bluish
  • 23,093
  • 23
  • 110
  • 171
  • 4
    Nice one...as I was struggling with handling the download file prompt and using jquery ajax..and this solution works perfectly for me ..+1 – swapnesh Mar 01 '13 at 08:49
  • 48
    Note that this requires the server to be setting a Content-Disposition header value of 'attachment', otherwise the browser will redirect to (and display) the response content – brichins Apr 24 '13 at 21:45
  • 24
    Or alternatively use `window.open(, '_blank');` to ensure that the download won't replace your current browser content (regardless of the Content-Disposition header). – Christopher King Aug 15 '14 at 15:38
  • 3
    this is good solution but i want to show loading bar while downloading. and this is not possible with same mechanism. – Yogesh Prajapati Aug 26 '14 at 04:56
  • 4
    The problem with this solution is that if the operation fails/the server returns an error, your page will be redirected to the error page. To solve that use the iFrame solution – kofifus Jun 30 '15 at 23:52
  • 1
    Given that Luke Madhanga's answer shows that using HTML5 you can do it using AJAX, could you edit out the "you can't" line in your answer? Your answer has so many more upvotes that it initially threw me off thinking it's still not possible. Not everyone might scroll down to read all the other answers. – Matthijs Wessels Feb 21 '19 at 10:19
  • 1
    what if I need a custom header? – besil Mar 22 '19 at 12:24
  • 9
    The real problem with this solution - question is about `POST` request. – Atomosk May 28 '19 at 03:45
  • this is a quick one, but i dont have a way of trigerring other things (like a status message) once the callback from backend has arrived. – Nikhil VJ Jul 15 '20 at 13:53
42

You can with HTML5

NB: The file data returned MUST be base64 encoded because you cannot JSON encode binary data

In my AJAX response I have a data structure that looks like this:

{
    result: 'OK',
    download: {
        mimetype: string(mimetype in the form 'major/minor'),
        filename: string(the name of the file to download),
        data: base64(the binary data as base64 to download)
    }
}

That means that I can do the following to save a file via AJAX

var a = document.createElement('a');
if (window.URL && window.Blob && ('download' in a) && window.atob) {
    // Do it the HTML5 compliant way
    var blob = base64ToBlob(result.download.data, result.download.mimetype);
    var url = window.URL.createObjectURL(blob);
    a.href = url;
    a.download = result.download.filename;
    a.click();
    window.URL.revokeObjectURL(url);
}

The function base64ToBlob was taken from here and must be used in compliance with this function

function base64ToBlob(base64, mimetype, slicesize) {
    if (!window.atob || !window.Uint8Array) {
        // The current browser doesn't have the atob function. Cannot continue
        return null;
    }
    mimetype = mimetype || '';
    slicesize = slicesize || 512;
    var bytechars = atob(base64);
    var bytearrays = [];
    for (var offset = 0; offset < bytechars.length; offset += slicesize) {
        var slice = bytechars.slice(offset, offset + slicesize);
        var bytenums = new Array(slice.length);
        for (var i = 0; i < slice.length; i++) {
            bytenums[i] = slice.charCodeAt(i);
        }
        var bytearray = new Uint8Array(bytenums);
        bytearrays[bytearrays.length] = bytearray;
    }
    return new Blob(bytearrays, {type: mimetype});
};

This is good if your server is dumping filedata to be saved. However, I've not quite worked out how one would implement a HTML4 fallback

Community
  • 1
  • 1
Luke Madhanga
  • 4,192
  • 1
  • 35
  • 40
  • 1
    The `a.click()` does not seem to work in firefox... Any idea? – bigpony Nov 14 '17 at 18:34
  • In some browsers you might need to add the `a` to the dom in order for this code to work and/or remove the `revokeObjectURL` part: `document.body.appendChild(a)` – bigpony Nov 14 '17 at 21:26
  • saved my day (and possibly a job too :) ) Not a javascript expert by any measure... more java guy. However, I have no idea why a simple "createObjectURL(new Blob([atob(base64)]))" doesn't work! It simply doesn't, while all instinct says it must. grrr... – apil.tamang Mar 21 '19 at 19:55
  • at line `var bytechars = atob(base64)` it throws an error `JavaScript runtime error: InvalidCharacterError`. I am using Chrome Version 75.0.3770.142 but i dont know, what is wrong here. – Muflix Aug 07 '19 at 09:23
30

The simple way to make the browser downloads a file is 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();
 }

This opens the browser download pop up.

João Marcos
  • 3,553
  • 1
  • 17
  • 14
27

1. Framework agnostic: Servlet downloading file as attachment

<!-- with JS -->
<a href="javascript:window.location='downloadServlet?param1=value1'">
    download
</a>

<!-- without JS -->
<a href="downloadServlet?param1=value1" >download</a>

2. Struts2 Framework: Action downloading file as attachment

<!-- with JS -->
<a href="javascript:window.location='downloadAction.action?param1=value1'">
    download
</a>

<!-- without JS -->
<a href="downloadAction.action?param1=value1" >download</a>

It would be better to use <s:a> tag pointing with OGNL to an URL created with <s:url> tag:

<!-- without JS, with Struts tags: THE RIGHT WAY -->    
<s:url action="downloadAction.action" var="url">
    <s:param name="param1">value1</s:param>
</s:ulr>
<s:a href="%{url}" >download</s:a>

In the above cases, you need to write the Content-Disposition header to the response, specifying that the file needs to be downloaded (attachment) and not opened by the browser (inline). You need to specify the Content Type too, and you may want to add the file name and length (to help the browser drawing a realistic progressbar).

For example, when downloading a ZIP:

response.setContentType("application/zip");
response.addHeader("Content-Disposition", 
                   "attachment; filename=\"name of my file.zip\"");
response.setHeader("Content-Length", myFile.length()); // or myByte[].length...

With Struts2 (unless you are using the Action as a Servlet, an hack for direct streaming, for example), you don't need to directly write anything to the response; simply using the Stream result type and configuring it in struts.xml will work: EXAMPLE

<result name="success" type="stream">
   <param name="contentType">application/zip</param>
   <param name="contentDisposition">attachment;filename="${fileName}"</param>
   <param name="contentLength">${fileLength}</param>
</result>

3. Framework agnostic (/ Struts2 framework): Servlet(/Action) opening file inside the browser

If you want to open the file inside the browser, instead of downloading it, the Content-disposition must be set to inline, but the target can't be the current window location; you must target a new window created by javascript, an <iframe> in the page, or a new window created on-the-fly with the "discussed" target="_blank":

<!-- From a parent page into an IFrame without javascript -->   
<a href="downloadServlet?param1=value1" target="iFrameName">
    download
</a>

<!-- In a new window without javascript --> 
<a href="downloadServlet?param1=value1" target="_blank">
    download
</a>

<!-- In a new window with javascript -->    
<a href="javascript:window.open('downloadServlet?param1=value1');" >
    download
</a>
Community
  • 1
  • 1
Andrea Ligios
  • 46,329
  • 24
  • 102
  • 208
24

I have created little function as workaround solution (inspired by @JohnCulviner plugin):

// creates iframe and form in it with hidden field,
// then submit form with provided data
// url - form url
// data - data to form field
// input_name - form hidden input name

function ajax_download(url, data, input_name) {
    var $iframe,
        iframe_doc,
        iframe_html;

    if (($iframe = $('#download_iframe')).length === 0) {
        $iframe = $("<iframe id='download_iframe'" +
                    " style='display: none' src='about:blank'></iframe>"
                   ).appendTo("body");
    }

    iframe_doc = $iframe[0].contentWindow || $iframe[0].contentDocument;
    if (iframe_doc.document) {
        iframe_doc = iframe_doc.document;
    }

    iframe_html = "<html><head></head><body><form method='POST' action='" +
                  url +"'>" +
                  "<input type=hidden name='" + input_name + "' value='" +
                  JSON.stringify(data) +"'/></form>" +
                  "</body></html>";

    iframe_doc.open();
    iframe_doc.write(iframe_html);
    $(iframe_doc).find('form').submit();
}

Demo with click event:

$('#someid').on('click', function() {
    ajax_download('/download.action', {'para1': 1, 'para2': 2}, 'dataname');
});
ndpu
  • 19,758
  • 4
  • 48
  • 66
  • That sends the data in a *very* strange way to the server though. I wonder if it could be altered to create compliant POST? – Shayne Jan 19 '14 at 21:44
16

I faced the same issue and successfully solved it. My use-case is this.

"Post JSON data to the server and receive an excel file. That excel file is created by the server and returned as a response to the client. Download that response as a file with custom name in browser"

$("#my-button").on("click", function(){

// Data to post
data = {
    ids: [1, 2, 3, 4, 5]
};

// Use XMLHttpRequest instead of Jquery $ajax
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    var a;
    if (xhttp.readyState === 4 && xhttp.status === 200) {
        // Trick for making downloadable link
        a = document.createElement('a');
        a.href = window.URL.createObjectURL(xhttp.response);
        // Give filename you wish to download
        a.download = "test-file.xls";
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
    }
};
// Post data to URL which handles post request
xhttp.open("POST", excelDownloadUrl);
xhttp.setRequestHeader("Content-Type", "application/json");
// You should set responseType as blob for binary responses
xhttp.responseType = 'blob';
xhttp.send(JSON.stringify(data));
});

The above snippet is just doing following

  • Posting an array as JSON to the server using XMLHttpRequest.
  • After fetching content as a blob(binary), we are creating a downloadable URL and attaching it to invisible "a" link then clicking it. I did a POST request here. Instead, you can go for a simple GET too. We cannot download the file through Ajax, must use XMLHttpRequest.

Here we need to carefully set few things on the server side. I set few headers in Python Django HttpResponse. You need to set them accordingly if you use other programming languages.

# In python django code
response = HttpResponse(file_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

Since I download xls(excel) here, I adjusted contentType to above one. You need to set it according to your file type. You can use this technique to download any kind of files.

Naren Yellavula
  • 5,664
  • 2
  • 27
  • 22
  • "We cannot download the file through Ajax, must use XMLHttpRequest". XMLHttpRequest is AJAX by definition. Otherwise great solution for modern web browsers. For IE, which doesn't support `HTMLAnchorElement.download`, I'm thinking of combining it with the proprietary [msSaveOrOpenBlob](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/samples/hh779016(v=vs.85)) method. – Tsahi Asher Oct 04 '18 at 06:49
15

Ok, based on ndpu's code heres an improved (I think) version of ajax_download;-

function ajax_download(url, data) {
    var $iframe,
        iframe_doc,
        iframe_html;

    if (($iframe = $('#download_iframe')).length === 0) {
        $iframe = $("<iframe id='download_iframe'" +
                    " style='display: none' src='about:blank'></iframe>"
                   ).appendTo("body");
    }

    iframe_doc = $iframe[0].contentWindow || $iframe[0].contentDocument;
    if (iframe_doc.document) {
        iframe_doc = iframe_doc.document;
    }

    iframe_html = "<html><head></head><body><form method='POST' action='" +
                  url +"'>" 

    Object.keys(data).forEach(function(key){
        iframe_html += "<input type='hidden' name='"+key+"' value='"+data[key]+"'>";

    });

        iframe_html +="</form></body></html>";

    iframe_doc.open();
    iframe_doc.write(iframe_html);
    $(iframe_doc).find('form').submit();
}

Use this like this;-

$('#someid').on('click', function() {
    ajax_download('/download.action', {'para1': 1, 'para2': 2});
});

The params are sent as proper post params as if coming from an input rather than as a json encoded string as per the previous example.

CAVEAT: Be wary about the potential for variable injection on those forms. There might be a safer way to encode those variables. Alternatively contemplate escaping them.

Shayne
  • 1,533
  • 1
  • 15
  • 17
  • This is working example. Thanks. Is it possible to do that without iframe but without window.location ? – Marek Bar Oct 15 '14 at 13:57
  • I suppose you could just append the hidden form to the bottom of the DOM. Also possibly worth exploring is use of the Shadow dom , although thats not necessarily well supported on older browsers. – Shayne Oct 17 '14 at 02:44
  • In this code I am getting this error. `Uncaught SecurityError: Blocked a frame with origin "http://foo.bar.com" from accessing a frame with origin "null". The frame requesting access has a protocol of "http", the frame being accessed has a protocol of "data". Protocols must match.` – void Jan 16 '16 at 10:09
  • How can I map this form to some model class? I have: `@ResourceMapping() public void downloadFile(final ResourceRequest request, final ResourceResponse response, @ModelAttribute("downForm") FormModel model)` but it's not working.. – bartex9 Jan 12 '17 at 09:23
  • void : That would likely be some sort of cross origin security issue. Thats probably a whole stack overflow question in and of it self. @bartex9 : That would depend heavily on what sort of framework your using. But the principle would be to take the name and path and store that, whilst pushing the file itself into a web accessible area of the file system, or something like amazon S3 for high availability – Shayne Jan 13 '17 at 02:23
9

Here is what I did, pure javascript and html. Did not test it but this should work in all browsers.

Javascript Function

var iframe = document.createElement('iframe');
iframe.id = "IFRAMEID";
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.src = 'SERVERURL'+'?' + $.param($scope.filtro);
iframe.addEventListener("load", function () {
     console.log("FILE LOAD DONE.. Download should start now");
});

Using just components that is supported in all browsers no additional libraries.

enter image description here enter image description here

Here is my server side JAVA Spring controller code.

@RequestMapping(value = "/rootto/my/xlsx", method = RequestMethod.GET)
public void downloadExcelFile(@RequestParam(value = "param1", required = false) String param1,
    HttpServletRequest request, HttpServletResponse response)
            throws ParseException {

    Workbook wb = service.getWorkbook(param1);
    if (wb != null) {
        try {
            String fileName = "myfile_" + sdf.format(new Date());
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setHeader("Content-disposition", "attachment; filename=\"" + fileName + ".xlsx\"");
            wb.write(response.getOutputStream());
            response.getOutputStream().close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    }
alexbt
  • 14,285
  • 6
  • 67
  • 82
manukyanv07
  • 333
  • 5
  • 4
  • it seems your load event is not called for Content-disposition attachment content (because nothing is loaded into the iframe), if it works for you (you get the console.log) pls post a sample – kofifus Jun 30 '15 at 06:33
  • Here is a quick fiddle https://jsfiddle.net/y2xezyoj/ this fires the load event ass soon as the pdf file is loaded into the iframe.. this fiddle does not download becaus the key for downloading is in the server side "response.setHeader("Content-disposition", "**attachment**; filename=\"" + fileName + ".xlsx\"");" – manukyanv07 Jun 30 '15 at 19:13
  • 1
    yes it will work in that case, but if the file is downloaded, that is the server sends Content-Disposition: attachment, then the load event will not fire which was my point – kofifus Jun 30 '15 at 23:38
  • You are totally right load event is fired right after the server is done processing starts sending the file. This is what i was looking for, 1- block the button and show processing so that the user can have a feedback that things are happening. 2 - Then when the server is done processing and about to send the file 3- (load event is fired) where I unlock the button and remove processing spinner 4 - the user is now poped up with save file or the browser starts download it in the defined download location. Sorry my English. – manukyanv07 Jul 02 '15 at 21:45
6

How to DOWNLOAD a file after receiving it by AJAX

It’s convenient when the file is created for a long time and you need to show PRELOADER

Example when submitting a web form:

<script>
$(function () {
    $('form').submit(function () {
        $('#loader').show();
        $.ajax({
            url: $(this).attr('action'),
            data: $(this).serialize(),
            dataType: 'binary',
            xhrFields: {
                'responseType': 'blob'
            },
            success: function(data, status, xhr) {
                $('#loader').hide();
                // if(data.type.indexOf('text/html') != -1){//If instead of a file you get an error page
                //     var reader = new FileReader();
                //     reader.readAsText(data);
                //     reader.onload = function() {alert(reader.result);};
                //     return;
                // }
                var link = document.createElement('a'),
                    filename = 'file.xlsx';
                // if(xhr.getResponseHeader('Content-Disposition')){//filename 
                //     filename = xhr.getResponseHeader('Content-Disposition');
                //     filename=filename.match(/filename="(.*?)"/)[1];
                //     filename=decodeURIComponent(escape(filename));
                // }
                link.href = URL.createObjectURL(data);
                link.download = filename;
                link.click();
            }
        });
        return false;
    });
});
</script>

Optional functional is commented out to simplify the example.

No need to create temporary files on the server.

On jQuery v2.2.4 OK. There will be an error on the old version:

Uncaught DOMException: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob').
Mike S
  • 172
  • 2
  • 7
  • To get the file name from Content-Disposition, this match worked for me: `filename.match(/filename=(.*)/)[1]` (without the double quotes or question mark) - https://regex101.com/r/2AsD4y/2. However, your solution was the only solution that worked after searching a lot. – jstuardo May 18 '20 at 15:05
  • Here is another lazier way to fetch the filename :) https://www.npmjs.com/package/content-disposition – Martin Kovachev Jul 07 '20 at 14:19
5
function downloadURI(uri, name) 
{
    var link = document.createElement("a");
    link.download = name;
    link.href = uri;
    link.click();
}
Wai Ha Lee
  • 7,664
  • 52
  • 54
  • 80
EL missaoui habib
  • 1,009
  • 1
  • 14
  • 24
  • Could you explain your answer? That'd help others understand what you've done so they could apply your techniques to their situations. – Wai Ha Lee Apr 12 '16 at 16:34
  • 2
    Just a warning: Safari and IE does not support the `download` attribute, so your file will end up having the name "Unknown" – Yangshun Tay May 16 '16 at 09:21
5

I try to download a CSV file and then do something after download has finished. So I need to implement an appropriate callback function.

Using window.location="..." is not a good idea because I cannot operate the program after finishing download. Something like this, change header so it is not a good idea.

fetch is a good alternative however it cannot support IE 11. And window.URL.createObjectURL cannot support IE 11.You can refer this.

This is my code, it is similar to the code of Shahrukh Alam. But you should take care that window.URL.createObjectURL maybe create memory leaks. You can refer this. When response has arrived, data will be stored into memory of browser. So before you click a link, the file has been downloaded. It means that you can do anything after download.

$.ajax({
    url: 'your download url',
    type: 'GET',
}).done(function (data, textStatus, request) {
    // csv => Blob
    var blob = new Blob([data]);

    // the file name from server.
    var fileName = request.getResponseHeader('fileName');

    if (window.navigator && window.navigator.msSaveOrOpenBlob) { // for IE
    window.navigator.msSaveOrOpenBlob(blob, fileName);
    } else { // for others
    var url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);

    //Do something after download 
    ...

    }
}).then(after_download)
}
kyakya
  • 887
  • 1
  • 9
  • 17
4

Adding some more things to above answer for downloading a file

Below is some java spring code which generates byte Array

@RequestMapping(value = "/downloadReport", method = { RequestMethod.POST })
    public ResponseEntity<byte[]> downloadReport(
            @RequestBody final SomeObejct obj, HttpServletResponse response) throws Exception {

        OutputStream out = new ByteArrayOutputStream();
        // write something to output stream
        HttpHeaders respHeaders = new HttpHeaders();
        respHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        respHeaders.add("X-File-Name", name);
        ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
        return new ResponseEntity<byte[]>(bos.toByteArray(), respHeaders, HttpStatus.CREATED);
    }

Now in javascript code using FileSaver.js ,can download a file with below code

var json=angular.toJson("somejsobject");
var url=apiEndPoint+'some url';
var xhr = new XMLHttpRequest();
//headers('X-File-Name')
xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 201) {
        var res = this.response;
        var fileName=this.getResponseHeader('X-File-Name');
        var data = new Blob([res]);
        saveAs(data, fileName); //this from FileSaver.js
    }
}    
xhr.open('POST', url);
xhr.setRequestHeader('Authorization','Bearer ' + token);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'arraybuffer';
xhr.send(json);

The above will download file

cheb1k4
  • 1,920
  • 5
  • 21
  • 37
dario nascimento
  • 517
  • 5
  • 11
4

In Rails, I do it this way:

function download_file(file_id) {
  let url       = '/files/' + file_id + '/download_file';
    $.ajax({
    type: 'GET',
    url: url,
    processData: false,
    success: function (data) {
       window.location = url;
    },
    error: function (xhr) {
     console.log(' Error:  >>>> ' + JSON.stringify(xhr));
    }
   });
 }

The trick is the window.location part. The controller's method looks like:

# GET /files/{:id}/download_file/
def download_file
    send_file(@file.file,
          :disposition => 'attachment',
          :url_based_filename => false)
end
aarkerio
  • 1,638
  • 1
  • 15
  • 29
4

Use window.open https://developer.mozilla.org/en-US/docs/Web/API/Window/open

For example, you can put this line of code in a click handler:

window.open('/file.txt', '_blank');

It will open a new tab (because of the '_blank' window-name) and that tab will open the URL.

Your server-side code should also have something like this:

res.set('Content-Disposition', 'attachment; filename=file.txt');

And that way, the browser should prompt the user to save the file to disk, instead of just showing them the file. It will also automatically close the tab that it just opened.

Andrew Koster
  • 987
  • 1
  • 14
  • 23
2

Ok so here is the working code when Using MVC and you are getting your file from a controller

lets say you have your byte array declare and populate, the only thing you need to do is to use the File function (using System.Web.Mvc)

byte[] bytes = .... insert your bytes in the array
return File(bytes, System.Net.Mime.MediaTypeNames.Application.Octet, "nameoffile.exe");

and then, in the same controller, add thoses 2 functions

protected override void OnResultExecuting(ResultExecutingContext context)
    {
        CheckAndHandleFileResult(context);

        base.OnResultExecuting(context);
    }

    private const string FILE_DOWNLOAD_COOKIE_NAME = "fileDownload";

    /// <summary>
    /// If the current response is a FileResult (an MVC base class for files) then write a
    /// cookie to inform jquery.fileDownload that a successful file download has occured
    /// </summary>
    /// <param name="context"></param>
    private void CheckAndHandleFileResult(ResultExecutingContext context)
    {
        if (context.Result is FileResult)
            //jquery.fileDownload uses this cookie to determine that a file download has completed successfully
            Response.SetCookie(new HttpCookie(FILE_DOWNLOAD_COOKIE_NAME, "true") { Path = "/" });
        else
            //ensure that the cookie is removed in case someone did a file download without using jquery.fileDownload
            if (Request.Cookies[FILE_DOWNLOAD_COOKIE_NAME] != null)
                Response.Cookies[FILE_DOWNLOAD_COOKIE_NAME].Expires = DateTime.Now.AddYears(-1);
    }

and then you will be able to call your controller to download and get the "success" or "failure" callback

$.fileDownload(mvcUrl('name of the controller'), {
            httpMethod: 'POST',
            successCallback: function (url) {
            //insert success code

            },
            failCallback: function (html, url) {
            //insert fail code
            }
        });
Yannick Richard
  • 1,079
  • 13
  • 24
1

I found a fix that while it's not actually using ajax it does allow you to use a javascript call to request the download and then get a callback when the download actually starts. I found this helpful if the link runs a server side script that takes a little bit to compose the file before sending it. so you can alert them that it's processing, and then when it does finally send the file remove that processing notification. which is why I wanted to try to load the file via ajax to begin with so that I could have an event happen when the file is requested and another when it actually starts downloading.

the js on the front page

function expdone()
{
    document.getElementById('exportdiv').style.display='none';
}
function expgo()
{
   document.getElementById('exportdiv').style.display='block';
   document.getElementById('exportif').src='test2.php?arguments=data';
}

the iframe

<div id="exportdiv" style="display:none;">
<img src="loader.gif"><br><h1>Generating Report</h1>
<iframe id="exportif" src="" style="width: 1px;height: 1px; border:0px;"></iframe>
</div>

then the other file:

<!DOCTYPE html>
<html>
<head>
<script>
function expdone()
{
    window.parent.expdone();
}
</script>
</head>
<body>
<iframe id="exportif" src="<?php echo "http://10.192.37.211/npdtracker/exportthismonth.php?arguments=".$_GET["arguments"]; ?>"></iframe>
<script>document.getElementById('exportif').onload= expdone;</script>
</body></html>

I think there's a way to read get data using js so then no php would be needed. but I don't know it off hand and the server I'm using supports php so this works for me. thought I'd share it in case it helps anyone.

Kit Ramos
  • 1,211
  • 1
  • 13
  • 30
1

If the server is writing the file back in the response (including cookies if you use them to determine whether the file download started), Simply create a form with the values and submit it:

function ajaxPostDownload(url, data) {
    var $form;
    if (($form = $('#download_form')).length === 0) {
        $form = $("<form id='download_form'" + " style='display: none; width: 1px; height: 1px; position: absolute; top: -10000px' method='POST' action='" + url + "'></form>");
        $form.appendTo("body");
    }
    //Clear the form fields
    $form.html("");
    //Create new form fields
    Object.keys(data).forEach(function (key) {
        $form.append("<input type='hidden' name='" + key + "' value='" + data[key] + "'>");
    });
    //Submit the form post
    $form.submit();
}

Usage:

ajaxPostDownload('/fileController/ExportFile', {
    DownloadToken: 'newDownloadToken',
    Name: $txtName.val(),
    Type: $txtType.val()
});

Controller Method:

[HttpPost]
public FileResult ExportFile(string DownloadToken, string Name, string Type)
{
    //Set DownloadToken Cookie.
    Response.SetCookie(new HttpCookie("downloadToken", DownloadToken)
    {
        Expires = DateTime.UtcNow.AddDays(1),
        Secure = false
    });

    using (var output = new MemoryStream())
    {
        //get File
        return File(output.ToArray(), "application/vnd.ms-excel", "NewFile.xls");
    }
}
Pierre
  • 6,347
  • 4
  • 48
  • 67
1

My approach is completly based on jQuery. The problem for me was that it has to be a POST-HTTP call. And I wanted it to be done by jQuery alone.

The solution:

$.ajax({
    type: "POST",
    url: "/some/webpage",
    headers: {'X-CSRF-TOKEN': csrfToken},
    data: additionalDataToSend,
    dataType: "text",
    success: function(result) {
        let blob = new Blob([result], { type: "application/octetstream" }); 

        let a = document.createElement('a');
        a.href = window.URL.createObjectURL(blob);
        a.download = "test.xml";;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        window.URL.removeObjectURL(a.href);
                        
        ...
    },
    error: errorDialog
});

Explanation:

What I and many others do is to create a link on the webpage, indicating that the target should be downloaded and putting the result of the http-request as the target. After that I append the link to the document than simply clicking the link and removing the link afterwards. You don't need an iframe anymore.

The magic lies in the lines

let blob = new Blob([result], { type: "application/octetstream" }); 
a.href = window.URL.createObjectURL(blob);

The interesting point is that this solution is only working with a "blob". As you can see in other answers, some are simply using a blob but not explaining why and how to create it. As you can read e.g. in the Mozilla developer documentation you need a file, media ressource or blob for the function "createObjectURL()" to work. The problem is that your http-response might not be any of those. Therefore the first thing you must do is to convert your response to a blob. This is what the first line does. Then you can use the "createObjectURL" with your newly created blob. If you than click the link your browser will open a file-save dialog and you can save your data. Obviously it s possible that you cannot define a fixed filename for your file to download. Then you must make your response more complex like in the answer from Luke.

And don't forget to free up the memory especially when you are working with large files. For more examples and information you can look at the details of the JS blob object

M46
  • 617
  • 6
  • 17
0

If you want to use jQuery File Download , please note this for IE. You need to reset the response or it will not download

    //The IE will only work if you reset response
    getServletResponse().reset();
    //The jquery.fileDownload needs a cookie be set
    getServletResponse().setHeader("Set-Cookie", "fileDownload=true; path=/");
    //Do the reset of your action create InputStream and return

Your action can implement ServletResponseAware to access getServletResponse()

Alireza Fattahi
  • 33,509
  • 12
  • 96
  • 140
0

It is certain that you can not do it through Ajax call.

However, there is a workaround.

Steps :

If you are using form.submit() for downloading the file, what you can do is :

  1. Create an ajax call from client to server and store the file stream inside the session.
  2. Upon "success" being returned from server, call your form.submit() to just stream the file stream stored in the session.

This is helpful in case when you want to decide whether or not file needs to be downloaded after making form.submit(), eg: there can be a case where on form.submit(), an exception occurs on the server side and instead of crashing, you might need to show a custom message on the client side, in such case this implementation might help.

Aman Srivastava
  • 111
  • 1
  • 4
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
The HTML Code:-

'<button type="button" id="GetFile">Get File!</button>'


The jQuery Code:-

'$('#GetFile').on('click', function () {
    $.ajax({
        url: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/172905/test.pdf',
        method: 'GET',
        xhrFields: {
            responseType: 'blob'
        },
        success: function (data) {
            var a = document.createElement('a');
            var url = window.URL.createObjectURL(data);
            a.href = url;
            a.download = 'myfile.pdf';
            document.body.append(a);
            a.click();
            a.remove();
            window.URL.revokeObjectURL(url);
        }
    });
});'
  • Code only answers should have at least a minumum description explaining how the code works and why it answers to the question. – Roberto Caboni Jan 03 '20 at 10:21
  • 1
    It gives me -> VM2821:81 Uncaught TypeError: Failed to execute 'createObjectURL' on 'URL': Overload resolution failed – fatherazrael Jan 17 '21 at 06:31
0

That's it works so fine in any browser (I'm using asp.net core)

            function onDownload() {

  const api = '@Url.Action("myaction", "mycontroller")'; 
  var form = new FormData(document.getElementById('form1'));

  fetch(api, { body: form, method: "POST"})
      .then(resp => resp.blob())
      .then(blob => {
          const url = window.URL.createObjectURL(blob);
        $('#linkdownload').attr('download', 'Attachement.zip');
          $('#linkdownload').attr("href", url);
          $('#linkdownload')
              .fadeIn(3000,
                  function() { });

      })
      .catch(() => alert('An error occurred'));



}
 
 <button type="button" onclick="onDownload()" class="btn btn-primary btn-sm">Click to Process Files</button>
 
 
 
 <a role="button" href="#" style="display: none" class="btn btn-sm btn-secondary" id="linkdownload">Click to download Attachments</a>
 
 
 <form asp-controller="mycontroller" asp-action="myaction" id="form1"></form>
 
 
        function onDownload() {
            const api = '@Url.Action("myaction", "mycontroller")'; 
            //form1 is your id form, and to get data content of form
            var form = new FormData(document.getElementById('form1'));

            fetch(api, { body: form, method: "POST"})
                .then(resp => resp.blob())
                .then(blob => {
                    const url = window.URL.createObjectURL(blob);
                    $('#linkdownload').attr('download', 'Attachments.zip');
                    $('#linkdownload').attr("href", url);
                    $('#linkdownload')
                        .fadeIn(3000,
                            function() {

                            });
                })
                .catch(() => alert('An error occurred'));                 

        }
Marinpietri
  • 92
  • 3
  • 8
-1

I struggled with this issue for a long time. Finally an elegant external library suggested here helped me out.

Eerik Sven Puudist
  • 1,388
  • 1
  • 14
  • 25