11

I try to download a file. The action is triggered by ajax() POST request. The request sends data in JSON format to the controller. The controller generates the file (bytes) and sends it back.

JavaScript:

function getLicenseFile() {
    $.ajax({
        type: 'POST',
        url: '<%=request.getContextPath()%>/licenses/rest/downloadLicenseFile',
        dataType: 'json',
        contentType: 'application/json;charset=UTF-8',
        data: ko.mapping.toJSON(licenseModel),
        success: function (data) {
            console.log("in sucess")
        },
        error:function (xhr, ajaxOptions, thrownError){
            console.log("in error")
        } 
    });
}  

Controller:

@RequestMapping(value = "/licenses/rest/downloadLicenseFile", method = RequestMethod.POST)
@ResponseStatus(value=HttpStatus.OK)
@ResponseBody
public void createLicenseFile(@Valid @RequestBody License license, HttpServletResponse response) throws Exception {

    logger.debug("Contoller License in: "+ license);

    byte[] licensedata = licenseEncodeDefaultService.createLicenseFile(license);
    logger.debug("licenseData: " + new String(licensedata));

    response.setHeader("Content-Disposition", "attachment; filename=\"" + license.getCustomer() + ".license\"");
    response.getOutputStream().write(licensedata);
    response.flushBuffer();
}


Problem

  • The Browser should open a download box, but it does not happen
  • The response is handled in the error: section of ajax function (but the HTTP Status is OK)

So what do I do wrong or what is the proper way to do this?

informatik01
  • 16,038
  • 10
  • 74
  • 104
derlinuxer
  • 574
  • 1
  • 6
  • 12

5 Answers5

17

Just send a URL of file in response and then "visit" it in your success callback.

function getLicenseFile() {
    $.ajax({
        type: 'POST',
        url: '<%=request.getContextPath()%>/licenses/rest/downloadLicenseFile',
        dataType: 'json',
        contentType: 'application/json;charset=UTF-8',
        data: ko.mapping.toJSON(licenseModel),
        success: function (data) {
            window.open(data.fileUrl);
            // or window.location.href = data.fileUrl;
        },
        error:function (xhr, ajaxOptions, thrownError) {
            console.log("in error");
        } 
    });
}

data.fileUrl should be set in response by server to say client where to get the file.

So your server will send a response with JSON like

{
    "fileUrl": "http://mysite.com/files/0123456789"
}
Eugene Naydenov
  • 7,165
  • 2
  • 25
  • 43
  • hmm, normally I don't want to save the file in file system on server side and also I want have a clean download URL. Just generate in Memory and send back the bytes. But my be I've to do it really with two controller methods. First method generate the file (save to server FS) and return the file name in JSON Format. And second method send the file data. So I can prevent to have a clean download path. I'll check that out. – derlinuxer Nov 23 '12 at 08:57
  • window.open(data.fileUrl); does not work in an asynchronous environment. window.location.href = data.fileUrl; works perfectly! – lekant Dec 10 '15 at 07:40
8

@will824 As you ask I'll post my own solution.

I used a workaround in controller and save the file temporarily in the files ystem (/tmp). I split up the function in 2 steps. Creating and downloading. This is not very nice but good enough for me.

Controller (creates a file, will be saved on the server file system):

@RequestMapping(value = "/licenses/rest", method = RequestMethod.PUT)
@ResponseStatus(value=HttpStatus.OK)
@ResponseBody
public String createLicenseFile(@Valid @RequestBody License license) throws Exception {

    // create encrypted license file and send the name back to view
    String fileName =  licenseEncodeDefaultService.createLicenseFile(license);
    return fileName;

}

Controller (downloads a file):

@RequestMapping(value = "/licenses/downloadFile/{file}", method = RequestMethod.GET)
public void downloadLicenseFile(@PathVariable("file") String file, HttpServletResponse response) throws Exception {

    // create full filename and get input stream
    File licenseFile = new File ("/tmp/" + file);
    InputStream is = new FileInputStream(licenseFile);

    // set file as attached data and copy file data to response output stream
    response.setHeader("Content-Disposition", "attachment; filename=\"" + file + ".license\"");
    FileCopyUtils.copy(is, response.getOutputStream());

    // delete file on server file system
    licenseFile.delete();

    // close stream and return to view
    response.flushBuffer();
}

JavaScript:

function getLicenseFile() {
    //console.log(ko.mapping.toJSON(licenseModel));
    $.ajax({
        type : 'PUT',
        url : '${pageContext.request.contextPath}/licenses/rest',
        dataType : 'text',
        contentType : 'application/json;charset=UTF-8',
        data : ko.mapping.toJSON(licenseModel),
        success : function(data) {
            window.location.href = '${pageContext.request.contextPath}/licenses/downloadFile/'
                    + data;
        },
        error : function(xhr, ajaxOptions, thrownError) {
            // error handling
        }
    });
}
informatik01
  • 16,038
  • 10
  • 74
  • 104
derlinuxer
  • 574
  • 1
  • 6
  • 12
  • Thanks a lot for your reply. I do agree this makes a 1 step simple process to go 2 times to server, but your solution is more or less similar to the one I planned to implement, nevertheless I was not allowed to go 2 times to server, so just had to go with the 1 step call by using an invisible form with an iframe and accept the risk that if there is some error the user will never know what happened. – will824 Aug 13 '13 at 16:54
4

If you want download file without change URL, you can call form.submit() programmatically instead of using AJAX.

JavaScript:

function downloadFileUsingForm(url) { 
    var form = document.createElement("form");
    form.method = "post";
    form.action = url;
    document.body.appendChild(form);
    form.submit();
    document.body.removeChild(form);
}
downloadFileUsingForm("/YourController/DownloadFile");

Controller:

[HttpPost]
public ActionResult DownloadFile()
{
    string content = "Some Values";
    byte[] bytes = System.Text.UTF8Encoding.UTF8.GetBytes(content);
    return File(bytes, System.Net.Mime.MediaTypeNames.Application.Octet, "file.txt");
}
informatik01
  • 16,038
  • 10
  • 74
  • 104
user4532418
  • 49
  • 1
  • 1
  • This is the best answer I have found for the case I am working on. – Poly Oct 12 '16 at 12:19
  • 1
    _As a side note:_ the question was about Spring MVC (hence Java), but your controller example is in **C#** and **.NET** – informatik01 Nov 13 '19 at 18:05
  • Submitting a form will not send a JSON, which was required in the questions, so this answer is incorrect. – Plamen Mar 25 '21 at 19:52
2

As the comments said you can't do it with an ajax call, but you can do it with plain Javascript.

function getLicenseFile() {
    var downloadUrl = "${pageContext.request.contextPath}/licenses/rest/downloadLicenseFile";
    // (optionally) provide the user with a message that the download is starting
    window.location.href = downloadUrl;
}

Note the use of ${pageContext.request.contextPath}, which is preferred over <%=request.getContextPath()%>.

nickdos
  • 8,348
  • 5
  • 30
  • 47
  • Thanks, but the file isn't already on server side. It will created dynamic with given JSON Data by post request. No JSON Data no file to download. – derlinuxer Nov 23 '12 at 09:00
  • Yep I missed that. In that case I'd use a standard form POST then. No JavaScript is needed. – nickdos Nov 25 '12 at 11:18
0

Ajax is not going to help you try with hidden form approach

<form action='../servletname' method='POST' id='formid'>
                <input type='hidden' value='' name='name' id='id'/>
                <input type='hidden' value=' ' name='name'  id='id' />
            </form>

pass you json through form field on click of of your download button submit form

$('#formid').submit();

then in server side

response.setContentType("application/vnd.ms-excel");
            response.setHeader("Content-Disposition", "attachment; filename=filnemae.fileformat");

 ServletOutputStream out = res.getOutputStream();

write on ouput stream then close or flush

if you are sending large data through post update postsize in server.xml

abhinavsinghvirsen
  • 1,853
  • 16
  • 25