Broken PDF-File when using $response->setFileToSend

Hi there,

currently I have a strange problem.

I am using the following lines:

$filename= "test.pdf";

$path = "/path/to/test.pdf"

$response = new \Phalcon\Http\Response();

$response->setHeader("Cache-Control", 'must-revalidate, post-check=0, pre-check=0');

$response->setHeader('Content-Disposition', 'inline; filename=' . urlencode($filename));

$response->setContentType('application/pdf');

$response->setFileToSend($path, null, false);

$response->send();

In Firefox it works fine and the PDF is shown properly.

But when I call the URL with this method in Chrome, Chrome says "Error while loading file".

I don't really know what the problem is. Do I have any mistake in my code?

Thanks a lot!

Best

edited Sep '16

Chrome tries to be 'smart' at times, but turns out to be stupid instead. Try to set Content-Length header. In Phalcon 3.x release, there's a method: public setContentLength (mixed $contentLength). For 2.0.x you need to set it as raw.

Also, you don't need to urlencode() your file name. Why would you do that at first place? That name is being generated on-the-fly, and does not ever touch web server so it doesn't need to be URL encoded. It is only a hint for a client browser how to save the file on a local computer.

Also, if you use Phalcon method setFileToSend(), you don't need to specify attachment header as it will be set by default. Source: https://github.com/phalcon/cphalcon/blob/master/phalcon/http/response.zep#L635

Last but not the least, provide here full HTTP response when you GET the file. It must be something wrong in a response.

Thanks for your answer!

I accomplished all the things you mentioned but error is still the same.

I am using Phalcon 2.0.x. Here is the code I tested:

    $path = "test.pdf";

    $filetype = mime_content_type($path);

    $filesize = filesize($path);

    $response = new \Phalcon\Http\Response();
    $response->setHeader("Cache-Control", 'must-revalidate, post-check=0, pre-check=0');
    $response->setHeader("Content-Length", $filesize);
    $response->setContentType('application/pdf');
    $response->setFileToSend($path, null, false);
    $response->send();

In FF and Safari it works well. Chrome still shows same error.

What do I do wrong? Don't really get it.

Thanks for your help!

Provide full HTTP header response when you GET the file (from Firefox where you said it's working fine), or provide URL here so we can check directly.

edited Oct '16

All from Firefox when I call URL which shows PDF:

Response Header

HTTP/1.1 200 OK

Cache-Control must-revalidate, post-check=0, pre-check=0

Connection Keep-Alive

Content-Length 5562052

Content-Type application/pdf

Date Sun, 02 Oct 2016 09:08:22 GMT

Expires Thu, 19 Nov 1981 08:52:00 GMT

Keep-Alive timeout=5, max=100

Pragma no-cache

Server Apache/2.4.10 (Unix) OpenSSL/1.0.1j PHP/5.5.19 mod_perl/2.0.8-dev Perl/v5.16.3

X-Powered-By PHP/5.5.19

Request Header

Accept text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8

Accept-Encoding gzip, deflate

Accept-Language de,en-US;q=0.7,en;q=0.3

Connection keep-alive

Cookie rhCookie=O%3A8%3A%22stdClass%22%3A3%3A%7Bs%3A13%3A%22hideCookieBar%22%3Bi%3A0%3Bs%3A7%3A%22guestid%22 %3Bs%3A32%3A%2271b6f9e624f37319293578584200a592%22%3Bs%3A8%3A%22referral%22%3Bs%3A1%3A%22P%22%3B%7D; intercom-id=3d67db7c-afff-4c2e-858f-28d22f77830e; intercom-id-ndp9i749=3d67db7c-afff-4c2e-858f-28d22f77830e; _atuvc=16%7C35; PHPSESSID=ucvtjkto738dr3onbshkmfgng6; ckglue_visit=1

Host localhost

Upgrade-Insecure-Requests 1

User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0

Hopefully this helps.

edited Oct '16

Try with this, it is much more simple:

//resolve shared service from static context
//NOTE this was just my example / test, you can use $this->response or whatever context you're in
       $response = static::$di->getShared('response'); 

       $absFilePath = APP_PATH . '/Zend-Continuous-Delivery-Blueprint-1013-ENa.pdf'; //will resolve to absolute file path
       //Note: entire absolute path and the file must have READ permission for app server
       $response->setContentLength(filesize($absFilePath)); //Optional, method exists only in Phalcon 3.0.x

       $response->setFileToSend($absFilePath, 'fileShownToClients_'.mt_rand(PHP_VERSION_ID, mt_getrandmax()).'.pdf'); // randomize file name each time


       !$response->isSent() && $response->send(); //Finally, sent out the response to the client

Will produce header response:

Connection: keep-alive
Content-Description: File Transfer
Content-Disposition: attachment; filename=fileShownToClients_158475187.pdf
Content-Length: 6814521
Content-Type: application/octet-stream
Date: Sun, 02 Oct 2016 21:12:15 GMT
Server: nginx
X-Powered-By: COBOL V5.1 revision 0 build 10/10/2 G; 32409. Run Time System RXCVF/AA0/00000N
content-transfer-encoding: binary

Your code isn't work...to get further, here is the URL which produces the problem:

https://www.reachhero.de/brand/influencerMarketingHandoutDownload?downloadToken=8c68f7589dd9bd4b2f9875784fa26f24

In FF and Safari it works fine, in Chrome it isn't shown.

My code is the same like before.

Thanks!

edited Oct '16

My code does work, I tested it prior to posting here. Try it for yourself. If it does not work, pay attention to the comments in the code (i.e. permissions etc).

Now the link you provided is an extermal resource, what's with that? You don't have control over it from your app.

When I use your code, the PDF will be downloaded. I don't want to download the PDF, it should be shown in browser.

The link I provided is the app. When you open this link in Chrome, PDF doesn't appear and error will be shown. In other browsers it's working.

What do I have to change in your code that PDF will only be shown and not downloaded?

edited Oct '16

That's whole another point/dimension. That's client side interpretation of HTTP response from server. You cannot force it 100% to behave like you want, since each browser has it's own internal PDF (JS) viewer and it will try to optimize as much on it's own etc.

edited Oct '16

This will work.

//resolve shared service from static context
//NOTE this was just my example / test, you can use $this->response or whatever context you're in
       $response = static::$di->getShared('response'); 

       $absFilePath = APP_PATH . '/Zend-Continuous-Delivery-Blueprint-1013-ENa.pdf'; //will resolve to absolute file path
       //Note: entire absolute path and the file must have READ permission for app server
       $response->setContentLength(filesize($absFilePath)); //Optional, method exists only in Phalcon 3.0.x

       $response->setContentType('application/pdf'); //this is mandatory if you want to trigger internal PDF JS viewer functionality of the browser

       $response->setFileToSend($absFilePath, 'fileShownToClients_'.mt_rand(PHP_VERSION_ID, mt_getrandmax()).'.pdf', 0x00); // randomize file name each time


       !$response->isSent() && $response->send(); //Finally, sent out the response to the client

P.S. If that's what you want, the most convinient way is to just link to the file and let web server do the job (i.e. https://www.reachhero.de/brand/influencerMarketingHandoutDownload.pdf), unless you have a specific reason to present it via application, then use the code above.

unfortunately this code doesn't work.

We are using Phalcon 2.0.x. There isn't any method like " $response->setContentLength(filesize($absFilePath));". So I am using my version with "setHeader".

Do you have any other ideas how I can resolve it?

Thanks a lot!

edited Oct '16

I stated that the method setContentLength() is optional, but it still good to use it.

For Phalcon 2.0.x use this:

$response->setHeader('Content-Length', filesize($absFilePath)); //Calculate Content-Length header (Phalcon 2.0.x)

The code I have posted should work 1:1 between Phalcon 2.0.x and 3.0.x with only difference being using new method

edited Oct '16

This is now the code:

$response = $this->response;

$absFilePath = $this->config->attachmentsDir . "Marketing-DE.pdf"; //will resolve to absolute file path

$response->setHeader('Content-Length', filesize($absFilePath)); //Calculate Content-Length header (Phalcon 2.0.x)

$response->setContentType('application/pdf'); //this is mandatory if you want to trigger internal PDF JS viewer functionality of the browser

$response->setFileToSend($absFilePath, 'fileShownToClients_'.mt_rand(PHP_VERSION_ID, mt_getrandmax()).'.pdf', 0x00); // randomize file name each time

!$response->isSent() && $response->send(); //Finally, sent out the response to the client

It's productive and online. To check it, call this URL:

https://www.reachhero.de/brand/influencerMarketingHandoutDownload?downloadToken=7dca89824cfabe6edb8a4607c5c27b5c

Isn't working in chrome. Don't really know where the problem is.

edited Oct '16

I see, it does not work in Chrome for me either.

Can you try with another, PDF file? For example, use this file: https://s3.amazonaws.com/info-mongodb-com/MongoDB_Datasheet.pdf

Your PDF file has all empty properties, and format is set to: PDF-0

I'm not sure if that can break Chrome's internal JS viewer. If it does, you'll have to change a way how you generate your PDF's or to fill out those properties.

Have you tried with another PDF file (not generated by your app)? If you generate files with TCPDF, that might be the root cause of Chrome's internal JS viewer.

Doesn't work. Internet Explorer 11 has the same problem. There I got a message:

"File does not begin with "%PDF-'. Local\EWH-2176-0

Do you have any suggestions?

edited Nov '16

You must have some output buffer before actual content. Try to catch PHP warnings like 'headers already sent' etc. using register_shutdown_function()

It might be something really simple like you have blank space somewhere which gets to output before your binary data.

Also, debug with curl (raw) to see the actual output of your application with that PDF files.

One more time - I already asked you - how do you generate those PDF files? Which lib/software? That might be the culprit too.

I'm going crazy, I found the solution!

The only thing you have to add is the following line at start of function:

$this->view->setRenderLevel(View::LEVEL_NO_RENDER);

Thanks for your help, you helped me a lot!

Hm. That means that your view was returning some output before your output headers were set/sent. That's why it is good to test via command line (curl).

In general, it is very wise to completely disable view component on such specific functions:

  $this->view->disable();

P.S. I'm glad you have finally solved it!