5

On my company's site we have some tables that we need to export to a csv file.
There are some varying parameters, so the csv file needs to be dynamically created on request.

My problem is that after clicking to download, the response hangs, and waits for the whole file to be created (which can take some time) and only then downloads the entire file in one instant.

I'm using AngularJS, so I'm using window.location = <url_for_file_download> In order to make the browser download the file.

On the server side I'm using Java Spring and I've followed all the instructions I could find on the web in order to create a file download controller.

My controller code is something like this:

@RequestMapping(value = "http://yada.yada.yada/csv/myFile.csv", method = RequestMethod.GET)
public @ResponseBody
void getCustomers(HttpServletResponse response,
                         @RequestParam(required = false) String someParameters) 
                         throws NotAuthorizedException, IOException {  
// set headers
setHeaders(response);
// generate writer
CSVWriter write = generateWriter(response);
// get data
List<String[]> data = getData();
// write and flush and all that
.
.
.
}

My code for setting the response headers are:

response.setContentType("text/csv;charset=utf-8");
response.setHeader("Content-Disposition","attachment; filename=\"" + fileName + ".csv\"");

I've also tried adding the following headers:

response.setHeader("Transfer-Encoding", "Chunked");
response.setHeader("Content-Description", "File Transfer");

and I've also tried setting the Content-type to "application/octet-stream".

Notice that I don't add a Content-length header, since the file doesn't exist yet, and is being written on the fly.

For writing the csv file I'm using OpenCSV and my code is as follows:

OutputStream resOs = response.getOutputStream();
OutputStream buffOs = new BufferedOutputStream(resOs);
OutputStreamWriter outputWriter = new OutputStreamWriter(buffOs,"UTF-8");
CSVWriter writer = new CSVWriter(outputWriter);

I iterate over the data and write it like so:

for (String[] row: data) {
    writer.writeNext(line);
}

(It's not exactly the code - but this is more or else what happens in the code)

And at the end I flush and close:

writer.flush();
writer.close();

I also tried flushing after each line I write.

So why isn't the file being transferred before it has all been written? Why is my browser (Google chrome) downloading the file in one instant after waiting a long time? And how can I fix this.

I hope I've added enough code, if there's something missing just please tell me and I'll try to add it here.

Thank you so much in advance.

EricP
  • 3,395
  • 3
  • 33
  • 46
jonny bordo
  • 549
  • 3
  • 7
  • 17

3 Answers3

3

Can you try returning a null value in your java

return null ;

Or you can try below code also 1. Jquery code upon clicking the submit button

$(document).ready( function() {
     $('#buttonName').click(function(e){
    $("#formName").submit();
     //alert("The file ready to be downloaded");

});
});

Your controller code

@RequestMapping(value="/name",method=RequestMethod.POST)                


public ModelAndView downloadCSV(ModelMap model,HttpSession session,@ModelAttribute(value="Pojo") Pojo pojo
            ,HttpServletRequest request,HttpServletResponse response){

----------------some code----------------

response.setContentType("application/csv");   
("application/unknown"); 
response.setHeader("content-disposition","attachment;filename =filename.csv"); 
ServletOutputStream  writer = response.getOutputStream();   
            logger.info("downloading contents to csv");

            writer.print("A");
            writer.print(',');
            writer.println("B");            

        for(int i=0;i<limit;i++){

                writer.print(""+pojo.get(i).getA());
                writer.print(',');
                writer.print(pojo.get(i).getB()); 
                writer.println();   
        }

        writer.flush();
        writer.close();

---------------some code-----------
return null;
}

Hope this helps

Thomas
  • 498
  • 4
  • 16
  • Thanks Thomas. I actually tried returning a null ModelAndView, but that didn't help. Regarding JQuery, I prefer to avoid using that since I'm using AngularJS. I did also try to download the file by appending a hidden iframe with the src set to the csv file generation. – jonny bordo Feb 23 '15 at 09:12
  • One quick question. What is the size of your downloaded csv File(in MBs).From your post I understand you are pulling out the data to be downloaded from DB tables.So when you execute that query manually how much time its taking to retrieve entire records – Thomas Feb 23 '15 at 10:07
  • That's the thing, I can't detect the file size in advance, since I'm generating the file on the fly. currently I'm working with small 200-300 K files, but in the future they can grow to MBs. Depending on the data. – jonny bordo Feb 23 '15 at 10:12
  • if its go beyond 20 MB and if you don't want your UI to wait for the entire data to be processed and downloaded , then I guess better option is wrap this logic in some Asynchronous call(Eg :Thread).Like if you are using thread , you can wrap up the csv download logic in run method .So that once the user press the download csv button , the request will be processed in the backend without impacting the UI part. – Thomas Feb 23 '15 at 12:43
  • I guess I"ll have to separate the process to two requests: one to start the file creation and save it to a static location, return the url, and the second to fetch the file. Thanks for the help Thomas. – jonny bordo Feb 23 '15 at 12:58
0

The Controller will wait for the response to be written before the response is send back to the client.

Here is a nice post with multiple approaches / options outlined

Downloading a file from spring controllers

This post talks about flushing the output periodically to help fasten the download.

how to download large files without memory issues in java

If all you are trying to do is let the user know that the file download is in progress and due soon, I think an Ajax progress status indicaor might be your solution.

  1. Trigger the ajax call to the back-end to generate the file

  2. Show progress indicator to the user while file is being generated server side

  3. once response is available, file is presented to the user.

I think something similar is being explored here download file with ajax() POST Request via Spring MVC

Hope this helps!

Thanks, Paul

Community
  • 1
  • 1
Paul John
  • 1,626
  • 1
  • 13
  • 15
  • Hi Paul. Thanks, I have actually seen that post. I'm actually pretty much using what they suggest. But like you said the controller is waiting for the response to be completely written before it is sent back, and that's exactly my problem. I want the file to be sent while being written. I'm not sure I see a solution to that on that post. – jonny bordo Feb 23 '15 at 09:35
  • HI Johnny, the response once committed cannot be modified. Your end goal is to try and download the file faster right? Or you do absolutely need to be able to "sort of stream" the file? – Paul John Feb 23 '15 at 10:33
  • Hi Paul, my problem is that the file might be big, and so it will have to take some time to download (I'm talking about 10 or maybe more seconds). My problem is that because the file downloads instantly, there is no indication for the user that the file was requested. That's why I want to stream the file, so it will start right away, and the user will see the progress in the browser's download page. – jonny bordo Feb 23 '15 at 11:59
  • how about making the call to the server an ajax call, and then showing the user a progress indicator while its waiting for server response. – Paul John Feb 23 '15 at 12:09
  • Thanks for the answer Paul. I can't exactly download the file with an ajax request, but I might really just have to divide it to two separate requests: one to start the file creation, and another to fetch the file from a static location. – jonny bordo Feb 23 '15 at 12:56
  • Cool. I think the post i mentioned above helps you do something very similar. – Paul John Feb 23 '15 at 13:05
0

I faced the same issue. The code that didn't work for me was

@RequestMapping(value = "/test")
 public void test(HttpServletResponse response)
            throws IOException, InterruptedException {
    response.getOutputStream().println("Hello");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("How");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("are");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("you");
    response.getOutputStream().flush();
}

The culprit was ShallowEtagHeaderFilter. When this filter is enabled the response is sent in one chunk. When this filter is diabled the response is send in multiple chunks.

From this thread Tomcat does not flush the response buffer it looks like another possible culprit can be GzipFilter

Community
  • 1
  • 1
medvedev1088
  • 3,645
  • 24
  • 42
  • 1
    Thanks @medvedev1088! This might just be the solution I needed. However, I faced this problem in my previous job, so I can't check to see if it works in the environment I was working in. I hope I could test this solution somewhere, and accept your answer. I'll do my best. Thanks! – jonny bordo Dec 25 '16 at 16:25