2

I'm making a website using the Symfony framework, on an ovh server. When an user changes page, I create a new XMLHttpRequest to avoid to reload all the page and improve user experience.

Everything works well, but I want to add a loading bar while the next page is loading asynchronously. Unfortunately the lengthComputable parameter was false. To go through this issue, I've set the header on Symfony3 just before sending the response with theses lines :

$length = strlen($event->getResponse()->getContent());
$event->getResponse()->headers->set('Content-Length', $length);
$event->getResponse()->headers->set('Accept-Ranges', "bytes");
$event->getResponse()->sendHeaders();

This trick works in my local development server, lengthComputable is set to true and I can calculate the current load percentage. But when I put everything on my remote dev server, lengthComputable is false again.

Here is the response header using theses four lines :

Accept-Ranges:bytes
Accept-Ranges:bytes
Cache-Control:no-cache
Cache-Control:no-cache
Content-Encoding:gzip
Content-Length:3100
Content-Length:3100
Content-Type:text/html; charset=UTF-8
Date:Sat, 28 Jan 2017 12:34:08 GMT
Server:Apache
Set-Cookie:PHPSESSID=***; path=/; HttpOnly
Vary:Accept-Encoding

(And yes, some parameters are present two times)

I thing this is related to cross origin header policy, but I can't found a solution.

I've tried to set others parameters like

$responseHeaders->set('Access-Control-Allow-Headers', 'content-type');

and even

$responseHeaders->set('Access-Control-Allow-Origin', '*');

I followed and tried theses links

Symfony2. I can't set Content-Length header

How can I access the Content-Length header from a cross domain Ajax request?

CORS with php headers

EDIT

I'm not using any proxy, and my server is hosted on OVH.

Here is my javascript code (nothing exceptional)

ajax_oReq = new XMLHttpRequest();
ajax_oReq.addEventListener("progress", progress, false);
ajax_oReq.addEventListener("load", complete, false);
ajax_oReq.addEventListener("error", error, false);
ajax_oReq.addEventListener("abort", error, false);

params = "ajax=1";
if(url.indexOf('?')!=-1){ params = "&"+params; }else{ params = "?"+params; }

ajax_oReq.open("GET", url+params, true);
ajax_oReq.send();

In my progress(evt) function, I can see that lengthComputable is false by logging the evt parameter.

In my complete(evt) function, I put evt.target.response in my corresponding div.

I just want to add that when I use this code, I receive the last progress event after about 500ms with a loaded length equal to the total length of the page (so 100% of the document, but lengthComputable is false and total is equal to 0 so I know it just because I look my headers when the loading is end) But it takes about 5 more seconds for the complete function to be called. I thing this is because the content length isn't known. Anyway, when I remove my 4 lines of code (the first ones), this issue disappear but I always don't have lengthComputable=true ...

Thanks for your time !

Community
  • 1
  • 1
  • Post your javascript. Are you testing with the same browser ? Is your dev server behind a proxy ? – Tom Tom Jan 28 '17 at 19:03
  • I edited my post, so no I tried with chrome, firefox, opera and safari, same result. And no, I'm not using a proxy, my server is hosted by OVH (pro offer) – Romaric Mollard Jan 29 '17 at 10:14
  • Are you using the same Content-Encoding as in local ? GZIP might be the problem here – Tom Tom Jan 29 '17 at 18:39
  • Actually I'm not sure to understand : my pages are html pages, so in local as in the remote server, I use plain text encoding. – Romaric Mollard Jan 29 '17 at 20:22
  • `Content-Encoding:gzip` means Apache is compressing the response with gzip, so the length you set in PHP might not be correct. Check the headers in localhost to see if `Content-Encoding` is the same value. – Tom Tom Jan 29 '17 at 21:00
  • You're right, I did not notice this in the header, so if Apache do this after Synfony sent the response, I can't get the correct `ContentLength` value I suppose, and it is possible that the header is compressed to so unreadable before decompression. So do you think that I can do what I want ? I mean a loading bar during page change ? (if not I'll use a factice loading bar, with an exponential percentage animation, or something...) – Romaric Mollard Jan 30 '17 at 06:48

1 Answers1

2

So the problem was that Apache's mod_deflate was compressing the response in GZIP, resulting in the length not being computable client side (because Apache chunks the response from what I've read).

The quick & dirty solution is to disable compression for that URL in your Apache settings. If you cannot, or do not want to in order to keep it optimized, there is a more elaborate solution taken from here

$event->getResponse()->headers->set('x-decompressed-content-length', $length);

then your progress function should look something like:

var progress = function (e) {
      var contentLength;
      if (e.lengthComputable) {
        contentLength = e.total;
      } else {
        contentLength = e.target.getResponseHeader('x-decompressed-content-length');
      }
      var progress = (e.loaded / contentLength) * 100;
    };

I'm not sure it will work that well tho, it depends if e.loaded is based on the compressed or decompressed response, but there are other possible leads for you on that page

You could also try this, taken from here, i will let you translate it to Symfony because I'm not sure of how you are setting your content but basically it is about gzipping the data yourself beforehand so that Apache does not do it.

    // checks if gzip is supported by client
    $pack = true;
    if(empty($_SERVER["HTTP_ACCEPT_ENCODING"]) || strpos($_SERVER["HTTP_ACCEPT_ENCODING"], 'gzip') === false) //replace $_SERVER part with SF method
    {
        $pack = false;
    }

    // if supported, gzips data
    if($pack) {
        $replyBody = gzencode($replyBody, 9, FORCE_GZIP); // Watch out 9 is strong compression
        // Set SF's Response content with gzipped content here
        header("Content-Encoding: gzip"); // replace that with appropriate SF method
    } else {
        // Set SF's Response content with uncompressed content here
    }

    // compressed or not, sets the Content-Length           
    header("Content-Length: " . strlen($replyBody)); // replace that with appropriate SF methods

So pretty much add the first two if in your controller, or wherever you set the Response's content and keep your Event Listener as is.

Those are the two the least "hacky" solutions I could find. Further than that, your guess is as good as mine, I haven't tried it before.

Community
  • 1
  • 1
Tom Tom
  • 3,680
  • 5
  • 35
  • 40
  • Setting `x-decompressed-content-length` worked, I'm surprised but `evt.progress` seems to be the uncompressed size of the loaded content, so it works well. Thank you very much ! – Romaric Mollard Jan 31 '17 at 08:06