Download a file by jQuery.Ajax

908,120

Solution 1

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.

Solution 2

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...."

Solution 3

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

Solution 4

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.

Solution 5

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>
Share:
908,120
hguser
Author by

hguser

Updated on January 26, 2022

Comments

  • hguser
    hguser over 2 years

    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?

  • AnthonyVO
    AnthonyVO almost 12 years
    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.
  • swapnesh
    swapnesh about 11 years
    Nice one...as I was struggling with handling the download file prompt and using jquery ajax..and this solution works perfectly for me ..+1
  • brichins
    brichins about 11 years
    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
  • Pierpaolo
    Pierpaolo about 11 years
    can this work to force the browser to show a save dialog/ribbon for .png files as well?
  • Kevin B
    Kevin B over 10 years
    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?
  • JimmyUK1
    JimmyUK1 over 10 years
    @KevinB the link in the answer explains it all
  • Kevin B
    Kevin B over 10 years
    @asgerhallas Sure, but that's completely useless if said link goes away.
  • Adrian
    Adrian over 10 years
    Can I use $.fileDownload with POST instead of GET request?
  • Kevin B
    Kevin B over 10 years
    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.
  • Royi Namir
    Royi Namir over 10 years
    great script , however it would've been better to check xmkHttpRequest support for file download , and then fallback to iframes.
  • John Culviner
    John Culviner over 10 years
    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?
  • Shayne
    Shayne over 10 years
    That sends the data in a very strange way to the server though. I wonder if it could be altered to create compliant POST?
  • osmanz
    osmanz about 10 years
    If I want to download an image from an API like: 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
  • Christopher King
    Christopher King almost 10 years
    Or alternatively use window.open(<url>, '_blank'); to ensure that the download won't replace your current browser content (regardless of the Content-Disposition header).
  • Yogesh Prajapati
    Yogesh Prajapati over 9 years
    this is good solution but i want to show loading bar while downloading. and this is not possible with same mechanism.
  • Marek Bar
    Marek Bar over 9 years
    This is working example. Thanks. Is it possible to do that without iframe but without window.location ?
  • Shayne
    Shayne over 9 years
    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.
  • Amiga500
    Amiga500 almost 9 years
    Sir, Your input: "Content-Disposition", "inline;.... saved the poor coder's day :)
  • kofifus
    kofifus almost 9 years
    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
  • manukyanv07
    manukyanv07 almost 9 years
    Here is a quick fiddle 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\"");"
  • kofifus
    kofifus almost 9 years
    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
    kofifus almost 9 years
    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
  • manukyanv07
    manukyanv07 almost 9 years
    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.
  • Sara N
    Sara N over 8 years
    why I get ** $(...).html(...).dialog is not a function** error in line 85 of js? should I add sth more than the js file ?
  • void
    void over 8 years
    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.
  • Wai Ha Lee
    Wai Ha Lee about 8 years
    Could you explain your answer? That'd help others understand what you've done so they could apply your techniques to their situations.
  • Yangshun Tay
    Yangshun Tay about 8 years
    Just a warning: Safari and IE does not support the download attribute, so your file will end up having the name "Unknown"
  • Royi Namir
    Royi Namir over 7 years
    I don't understand : where does it says that cookie is written after the file has been downloaded. this is not a deterministic order.
  • StanE
    StanE over 7 years
    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.
  • John Culviner
    John Culviner over 7 years
    @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)
  • bartex9
    bartex9 over 7 years
    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..
  • Shayne
    Shayne over 7 years
    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
  • fabio.sang
    fabio.sang over 6 years
    Thank you, I used this solution. Worked like a charm. Also, if you don't get a blob from the response, just create a new Blob.
  • 0x777
    0x777 over 6 years
    The a.click() does not seem to work in firefox... Any idea?
  • 0x777
    0x777 over 6 years
    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)
  • coderhs
    coderhs almost 6 years
    Quick question, won't this generate the file twice? Once You send the ajax request. Then you make the page redirect to the same URL as well. How can we eliminate that?
  • aarkerio
    aarkerio almost 6 years
    Not in my case. I only tested it on Chrome though.
  • startsWith_R
    startsWith_R almost 6 years
    A better version with IE handling link
  • Sven
    Sven almost 6 years
    As coderhs already states correctly, the action gets called twice.
  • Tsahi Asher
    Tsahi Asher over 5 years
    "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 method.
  • PersyJack
    PersyJack over 5 years
    @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
  • Matthijs Wessels
    Matthijs Wessels about 5 years
    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
    Matthijs Wessels about 5 years
    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.
  • apil.tamang
    apil.tamang about 5 years
    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...
  • besil
    besil about 5 years
    what if I need a custom header?
  • Andrew Koster
    Andrew Koster almost 5 years
    This is the only answer that mentions "window.open" (one of the comments mentions it).
  • Atomosk
    Atomosk almost 5 years
    The real problem with this solution - question is about POST request.
  • Muflix
    Muflix almost 5 years
    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
    Muflix almost 5 years
    It does not work if you have a lot of parameters, because you will get too long url error.
  • alexventuraio
    alexventuraio over 4 years
    The link from @startsWith_R really helps if you are working with IE11
  • Roberto Caboni
    Roberto Caboni over 4 years
    Code only answers should have at least a minumum description explaining how the code works and why it answers to the question.
  • CSquared
    CSquared over 4 years
    It's getting called twice for me too.
  • PaulCo
    PaulCo about 4 years
    @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
  • John Culviner
    John Culviner about 4 years
    Anything that would use up a signifigant amount of RAM on the device that's downloading it.
  • jstuardo
    jstuardo almost 4 years
    To get the file name from Content-Disposition, this match worked for me: filename.match(/filename=(.*)/)[1] (without the double quotes or question mark) - regex101.com/r/2AsD4y/2. However, your solution was the only solution that worked after searching a lot.
  • Martin Kovachev
    Martin Kovachev almost 4 years
    Here is another lazier way to fetch the filename :) npmjs.com/package/content-disposition
  • Nikhil VJ
    Nikhil VJ almost 4 years
    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.
  • fatherazrael
    fatherazrael over 3 years
    It gives me -> VM2821:81 Uncaught TypeError: Failed to execute 'createObjectURL' on 'URL': Overload resolution failed
  • EM0
    EM0 about 3 years
    Very useful, thank you! Maybe the window.URL.revokeObjectURL(url); should be in a finally block?
  • scotts
    scotts over 2 years
    Thanks! But It should be: revokeObjectURL() window.URL.revokeObjectURL(a.href);
  • Zoey
    Zoey over 2 years
    I'm returning a zip file but when I use this method my zip file is invalid. I had to remove dataType from the ajax call to get it to work. For the blob creation I used application/zip, the option you used above, and tried removing it altogether.
  • M46
    M46 over 2 years
    @scotts Thx. I've changed my code.