2

I currently have the following working piece of code (angular but applies to any JS framework):

var url = '/endpoint/to/my/file';

$http({
  method: 'GET',
  url: url
})
.success(function(jdata) {
  window.location = url;
})
.error(function(je){
  // display errors on page
});

The above is called after a form was completed and the user has clicked on "submit" (the real situation is a bit more complex than this but it is the same idea). I do the form check asynchronously, so there's no page reload.

If the request is successful, returns a binary (a pdf file), if not succesful, the request returns a 400 BadRequest with errors formatted in JS. So what I do is, if successful, I redirect to the same url to have the PDF otherwise I get the JSON error object and do something with it.

How can I refrain from making two requests if the requests is successful?

  • Note1: on the backend side I would like to keep only one route that does everything, check + return PDF
  • Note2: the current situation is pretty neat in my opinion, since I have an asynchronous form check and if successful the file downloads directly in the browser since I have "CONTENT-DISPOSITION" -> "attachment" in the HTTP header of the successful response

Update: additional information about the architecture as requested by Emile: In my use case I have one endpoint that checks inputs (and other external requirements). For security reasons I cannot output the PDF if all requirements are not satisfied so I have to do the check prior to delivering the file ( the file is automatically generated) anyway. So having two endpoints would just be redundant and add some unnecessary complexity.

While writing I think an alternative solution could be to pass an argument on the endpoint while doing the check, so that if successful, it stops and does not generate the PDF, and then redirect to the same endpoint without the flag which will output the PDF. So I do the check twice but only load (and generate - which is resource intensive) the file only once and I have only one endpoint...

Here's the adapted code:

var url = '/endpoint/to/my/file';

$http({
  method: 'GET',
  url: url+'?check'
})
.success(function(jdata) {
  window.location = url;
})
.error(function(je){
  // display errors on page
});

On the backend side (I use Play framework/Scala)

def myendpoint(onlyDoCheck: Boolean = false) = Action{implicit request =>
   myForm.bindFromRequest.fold(
     e => BadRequest(myErrors),
     v => if(onlyDoCheck) Ok(simpleOkResponse) else  Ok(doComputationgeneratefileandoutputfile)
   )
}
John
  • 4,786
  • 8
  • 35
  • 44
  • 1
    have you seen: http://stackoverflow.com/questions/20830309/download-file-using-an-ajax-request – Iceman Jul 29 '16 at 19:14
  • url is in an object closure and methods are chain. I haven't used angular, but could you jump to that location url object with: window.location = this.url; – Leroy Thompson Jul 29 '16 at 19:16
  • I did not but the answer to the question is actually my question ... see user1447679's comment on Apr 30 '15 at 1:13 – John Jul 29 '16 at 19:16
  • @LeroyThompson this is what i do currently, but it does two requests... not too nice, see also my previous comment – John Jul 29 '16 at 19:18
  • @John so, you are saying: currently you are doing a location redirect for the pdf, but want to do it at the ajax success itself? – Iceman Jul 29 '16 at 19:21
  • 1
    @iceman If I understood you right, yes. I edited the question for more clarity – John Jul 29 '16 at 19:24
  • @John i dont think it is possible to start download from ajax. the html5 "download" attribute holds promise but how compatible it is, is questionable. – Iceman Jul 29 '16 at 19:30
  • http://stackoverflow.com/questions/14774282/force-download-via-ajax-and-php also mentions similar problem. – Iceman Jul 29 '16 at 19:31
  • 1
    See my edited answer to understand how to handle the response – Emile Bergeron Jul 29 '16 at 19:35
  • 1
    @John 's answer using HTML5 `download` is probably your only soln. Note that Safari, IE, Safari-iOS, Opera-mini and some such browsers dont support this particular spec. – Iceman Jul 29 '16 at 19:48

1 Answers1

9

The real deal

The best you could do is split your endpoint.

  1. One for the form and the convenience of having errors without refresh.
  2. Then, on success, redirect to your other endpoint which only downloads the file.

If the file was on the disk and wasn't auto-generated and required to be authenticated to be downloaded, you could hide the file behind a normal endpoint, do the checks, and return the file using X-Accel-Redirect header with nginx or X-Sendfile using apache.

The hack

Disclaimer: This is more of a hack than the best solution. As mention by @Iceman, Safari, IE, Safari-iOS, Opera-mini and some such browsers don't support this particular spec.

In your server-side endpoint, if the file is available without errors, you can set the header to the content-type of the file (like 'application/pdf') so the download will starts automatically.

If there are errors, don't set the header and return a json of the errors to inform the user through javascript.

Since we don't know what's behind, here's a python (Django) example:

response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename=your_filename.pdf'
response.write(repport.printReport(participantId))
return response

You can handle the response in the ajax success callback:

$.ajax({
    url: 'endpoint.php',
    success: function(data) {
        var blob = new Blob([data]);
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = "filename.pdf";
        link.click();
    }
});

You could also try the jQuery fileDownload plugin mentioned in this answer.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • 1
    It is an ajax requests, the download will begin but async and not available to the end user. – John Jul 29 '16 at 19:21
  • @John you need to handle the response, but it works, just tested it with php. – Emile Bergeron Jul 29 '16 at 19:36
  • 1
    @EmileBergeron could you also mention in the post about the browser support limitation warning, and the slight insight about that you are using html5 download spec – Iceman Jul 29 '16 at 19:50
  • 1
    @Iceman yes, and I do realise it's more a hack than a clear solution. – Emile Bergeron Jul 29 '16 at 19:52
  • Cheers, I think it is the best answer at the moment. Also, if u cud, just accomodate a fallback in case the spec is not available. – Iceman Jul 29 '16 at 19:55
  • @Iceman, do you have something in mind? All that I see is splitting the endpoint but that's against OP's requirements... Or using what OP is using, the window location? – Emile Bergeron Jul 29 '16 at 19:57
  • 1
    @EmileBergeron Just thought that it will save a request where possible and in other cases fallback and work but with the cost of an additional request. But, yes u r right. leave it as it is, coz its against OP's requirements – Iceman Jul 29 '16 at 20:00
  • 1
    @EmileBergeron Thanks, I believe this is what I was looking for. Too bad it only is a "hack". – John Jul 29 '16 at 20:12
  • @John I added the solution I would use, but I do understand it violates your requirements. – Emile Bergeron Jul 29 '16 at 20:13
  • @EmileBergeron thanks for the recommended approach, it is however exactly what I do not want to do ... ;) – John Jul 29 '16 at 20:15
  • @John, could you explain in your question why you don't want to do that? Is that your form data really heavy? – Emile Bergeron Jul 29 '16 at 20:17
  • 1
    @John, thanks for the update! I think your idea with a flag is the best of both ways. It's like two endpoint, but behind, it's the same for you. And yes, you should generate the PDF *only when* everything validates. – Emile Bergeron Jul 29 '16 at 20:39