From gary at devgurus.com Mon Jan 20 16:15:59 2014 From: gary at devgurus.com (Gary Smith) Date: Tue, 21 Jan 2014 11:15:59 +1100 Subject: [Melbourne-pm] Forcing a download Message-ID: <52DDBC3F.7020003@devgurus.com> Hi All, As this is my first post to the list I'll do a quick bio. I've been using Perl since v3 - that was for my ISP business where I wrote our in-house accounting system for dialup clients in Perl back in the late 90's. Since then I'va also used it to do the heavy lifting on a hit counter, sorting 6-8mil hits per day into a cluster of MySQL servers and various other smaller jobs. Fast forward to today and I'm working on a project that is Catalyst/Postgresql/Starman based and I've struck a problem that is quite irritating as it's a simple task that looks right but doesn't act as it should. I'm creating a file and then I want it to pop up a dialog to download that file. The request is posted via jquery/ajax, the file is generated and it then sends it back to the browser with some appropriate headers. Here's the code: Javascript/JQuery =========== Pretty straight forward - it posts to the function that is going to create the export file. The Catalyst subroutine =============== sub myexport_export :Chained('object') PathPart('myexport_export') Args(0) { my ( $self, $c ) = @_; $self->check_some stuff($c); my $result = try { $self->_do_myexport($c); } catch { $c->log->error("things_export: $_"); $c->response->code('500'); $c->response->body("$_"); }; my ($myexport_fh, $myexport_fname) = tempfile(); $myexport_fh->print($result); close($myexport_fh); if (-e $myexport_fname and -s $myexport_fname) { open $myexport_fh, "<". $myexport_fname; $c->response->header('Content-Type' => 'text/csv'); $c->response->header('Content-Disposition' => qq[attachment;filename="export.csv"]); $c->response->body($myexport_fh); unlink($myexport_fname); } else { # TODO: Error display here! } } There's a call to _do_myexport in there that extracts and returns some data, and then we create the tempfile and (hopefully) send it back to the browser. The Result ======= Chrome is telling me that the Content-Type, Length and Disposition headers are being sent back, but the browser isn't popping up a download dialog: 1. Request URL: http://10.211.55.5:5000/myexport/id/210/myexport_export 2. Request Method: POST 3. Status Code: 200 OK 4. Request Headersview source 1. Accept: */* 2. Accept-Encoding: gzip,deflate,sdch 3. Accept-Language: en-US,en;q=0.8 4. Connection: keep-alive 5. Content-Length: 6 6. Content-Type: application/x-www-form-urlencoded; charset=UTF-8 7. Cookie:myexport_session=9b8eca81032e0dc3c906d701974ff7e42d841a9f 8. Host: 10.211.55.5:5000 9. Origin: http://10.211.55.5:5000 10. Referer: http://10.211.55.5:5000/myexport/id/210/my_button_page 11. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36 12. X-Requested-With: XMLHttpRequest 5. Form Dataview sourceview URL encoded 1. id: 210 6. Response Headersview source 1. Connection: keep-alive 2. Content-Disposition: attachment;filename="export.csv" 3. Content-Length: 124 4. Content-Type: text/csv 5. Date: Tue, 21 Jan 2014 00:01:17 GMT 6. Set-Cookie: myexport_session=9b8eca81032e0dc3c906d701974ff7e42d841a9f; path=/; expires=Tue, 21-Jan-2014 02:01:17 GMT; HttpOnly 7. X-Catalyst: 5.90053 I have a nasty feeling that I'm missing something obvious here but I've gone over this so many times that I think/hope a fresh set of eyes will help find the problem. The data does get sent back to the browser but as data rather than a file to download. Regards, Gary Smith 1. -------------- next part -------------- An HTML attachment was scrubbed... URL: From tjc at wintrmute.net Mon Jan 20 16:50:06 2014 From: tjc at wintrmute.net (Toby Wintermute) Date: Tue, 21 Jan 2014 11:50:06 +1100 Subject: [Melbourne-pm] Forcing a download In-Reply-To: <52DDBC3F.7020003@devgurus.com> References: <52DDBC3F.7020003@devgurus.com> Message-ID: On 21 January 2014 11:15, Gary Smith wrote: > Hi All, > > As this is my first post to the list I'll do a quick bio. I've been using > Perl since v3 - that was for my ISP business where I wrote our in-house > accounting system for dialup clients in Perl back in the late 90's. Since > then I'va also used it to do the heavy lifting on a hit counter, sorting > 6-8mil hits per day into a cluster of MySQL servers and various other > smaller jobs. > > Fast forward to today and I'm working on a project that is > Catalyst/Postgresql/Starman based and I've struck a problem that is quite > irritating as it's a simple task that looks right but doesn't act as it > should. I'm creating a file and then I want it to pop up a dialog to > download that file. The request is posted via jquery/ajax, the file is > generated and it then sends it back to the browser with some appropriate > headers. Here's the code: Hi Gary, I noticed some issues in your code; I don't know if any of them are actually causing the issue you're facing, but it can't hurt to fix them just in case. 1) If _do_my_export() fails inside the try/catch block, you'll log the errors but then continue executing the rest of the code that relies upon $result being set. (except it won't be what you expect -- it'll be set to the return value from $c->response->body) 2) In some error cases, you'll never clean up your temporary files. Try using File::Temp->new, which will automatically clean up the file once the reference goes out of scope. 3) Put the second half of your routine into the try/catch block as well, and die out of it if you fail to open the file, or the file is zero bytes, rather than fall through to that empty TODO block. That way you can get free error handling for it -- and it may well explain why your code isn't working properly if that's where the error is. However.. I don't think browsers like accepting file downloads via ajax requests like that. Can you try this instead: Make the ajax post query as you are now, but instead of returning the file data, return a URL that, when accessed, returns the file data. Then on the webpage, redirect the page to that URL. ie. in your $.post() method, the success handler would be something like function(data) { if (data.success) { window.document.href = data.url;} else { show_message_to_user(data.error_message); } } Cheers, Toby From gary at devgurus.com Mon Jan 20 20:13:00 2014 From: gary at devgurus.com (Gary Smith) Date: Tue, 21 Jan 2014 15:13:00 +1100 Subject: [Melbourne-pm] Forcing a download In-Reply-To: References: <52DDBC3F.7020003@devgurus.com> Message-ID: <52DDF3CC.5050600@devgurus.com> Hi Toby, Thanks for the tips - I'd like to say 'hey, I'm a little rusty but I would have picked up those issues' ... but I'd be lying other than the rusty part, so I really appreciate your feedback :). You were right about the ajax call. I was able to change the code so that it redirects directly to the URL that provides the download and it's now working perfectly. Regards, Gary On 21/01/2014 11:50 am, Toby Wintermute wrote: > On 21 January 2014 11:15, Gary Smith wrote: >> Hi All, >> >> As this is my first post to the list I'll do a quick bio. I've been using >> Perl since v3 - that was for my ISP business where I wrote our in-house >> accounting system for dialup clients in Perl back in the late 90's. Since >> then I'va also used it to do the heavy lifting on a hit counter, sorting >> 6-8mil hits per day into a cluster of MySQL servers and various other >> smaller jobs. >> >> Fast forward to today and I'm working on a project that is >> Catalyst/Postgresql/Starman based and I've struck a problem that is quite >> irritating as it's a simple task that looks right but doesn't act as it >> should. I'm creating a file and then I want it to pop up a dialog to >> download that file. The request is posted via jquery/ajax, the file is >> generated and it then sends it back to the browser with some appropriate >> headers. Here's the code: > Hi Gary, > I noticed some issues in your code; I don't know if any of them are > actually causing the issue you're facing, but it can't hurt to fix > them just in case. > > 1) If _do_my_export() fails inside the try/catch block, you'll log the > errors but then continue executing the rest of the code that relies > upon $result being set. (except it won't be what you expect -- it'll > be set to the return value from $c->response->body) > > 2) In some error cases, you'll never clean up your temporary files. > Try using File::Temp->new, which will automatically clean up the file > once the reference goes out of scope. > > 3) Put the second half of your routine into the try/catch block as > well, and die out of it if you fail to open the file, or the file is > zero bytes, rather than fall through to that empty TODO block. That > way you can get free error handling for it -- and it may well explain > why your code isn't working properly if that's where the error is. > > However.. I don't think browsers like accepting file downloads via > ajax requests like that. > Can you try this instead: Make the ajax post query as you are now, but > instead of returning the file data, return a URL that, when accessed, > returns the file data. Then on the webpage, redirect the page to that > URL. > ie. in your $.post() method, the success handler would be something like > function(data) { > if (data.success) { window.document.href = data.url;} > else { show_message_to_user(data.error_message); } > } > > Cheers, > Toby -------------- next part -------------- An HTML attachment was scrubbed... URL: