Caching at HTTP-Level

Hello, there is what I think a big bug in your implementation because it wont allow to cache pages in deed, you are setting Cache-Control in prefiltering, but it get overwrited to default by the call to function "return ob_get_clean();" in line 128 of CBaseController.php.

It took me some time to dig on this.

To fix it you only has to call to sendCacheControlHeader function on postFilter() overwrited function in your CHttpCachedFilter class.

I hope this helps.

regards,

alex

Well, with this fix I’m having some problems rendering some pages like “contact” or “login”. It throws the typical error message:

Cannot modify header information - headers already sent by (output started at E:\EasyPHP\www\yii\framework\web\CController.php:794)

[i]

[/i]

Anyway, I couldnt get a "cached" page yet. I try it turning off my connection and refreshing but no way!!

Does some body actually got this http cache really working?

Strange, I haven’t had this yet. Can you show me the filters() method of your controller?

Ok I’ve got the problem and bug, let me explain and give you a solution:

The problem:

Whenever you use the “User session” for example in the scafolded default Top Menu (when it uses Yii::app()->user) the internal code (at line 115 of CHttpSession for example, and maybe in other locations) call the php function session_start(), and according to PHP manual, “This function sends out several HTTP headers depending on the configuration”, among that it clears the “Cache control” header, so the header of your HTTP class filter gets overwrited and doesn’t work. In the same manual it says you can customize or parametrice this cache control header if you call to function cache_session_limiter in every request before start the session.

The fix:

To fix this bug you can write this code in your class:




	/**

	 * Http cache control headers. Set this to null in order to keep this

	 * header from being sent entirely.

	 * @var string

	 */

	public $cacheControl = null;

        

        const CACHE_LIMITER_PUBLIC = 'public';

        const CACHE_LIMITER_PRIVATE_NO_EXPIRE = 'private_no_expire';

        const CACHE_LIMITER_PRIVATE = 'private';

        const CACHE_LIMITER_NOCACHE = 'nocache';



and in the function to write the cache control header:





	/**

	 * Sends the cache control header to the client

	 * @see cacheControl

	 * @since 1.1.12

	 */

	protected function sendCacheControlHeader()

	{

                session_cache_limiter($this->cacheControl);

                

	}



This code is working for me perfectly.

This bug took me some hours to “chase and dig in the code” so I hope it helps :D

regards

Thanks for reporting this. I’ll patch the filter asap.

you are welcome!

Damn, I messed up, I’m afraid. This won’t be that easy to fix :(

why not? you are only setting the [size=2]session_cache_limiter arent you?[/size]

Well, that fix won’t work if there has been no session started. And I need to figure out a way to incorporate this change without the introduction of BC-breaking behaviours. So let’s just say I’ll need a bit.

Hi, first of all thanks to Da:Sourcerer for this extension.

I’ve been trying to use it, and probably also because of my little knowledge on a couple of subjects, I’m having an hard time to understand how to better use the etagSeed, as there’s little if no evidence on how to use it correctly on this website.

Currently I have no problem in setting it to any string, but anything more complex (for instance the name of the action using $this->action->id) will throw an error as the filter is evaluated at the very beginning.

Is it something I should be worried about or that’s simply enough to have anything in there? (i.e. are pages served with the same etag OK or is it something not exactly correct?)

Second question: on one of my actions I can’t get the Cache-Control to be set. The browser request contains a Cache-Control: max-age=0; (for reasons to me unknown) and this seems to cause Cache-Control to be returned with value: no-store, no-cache, must-revalidate, post-check=0, pre-check=0. I still don’t know who’s outputting it (default apache behaviour?). I wonder if this is the same problem reported by Aleksdj.

Best,

peach.

I take it you are trying to set CHttpCacheFilter.etagSeed? Try using CHttpCacheFilter.etagSeedExpresiion instead. It will be evaluated directly before calling an action.

I think this is due to the same problem aleksdj described. Could you perhaps c&p your settings for that filter here?

current configuration for the filter is the following


    public function filters() {

        return array(

            array(

                'CHttpCacheFilter',

                'cacheControl' => 'private; max-age=31536000',

                'etagSeedExpression' => 'Yii::app()->getController()->action->id',

            ),

        );

    }

Now the etagSeedExpression sets the etag for each single actions, which is fine.

Still there’s only one action which ignores it completely (Apparently as specified before).

As a side note, do you think it’s worth adding Vary header in this filter? Or would it be a better place for it?

This is a bit problematic. Vary can contain many things and we do not (yet) have an infrastructure for managing response headers, so there’s a chance of overwriting something important.

Working on a patch now, btw.

I see what you mean… and that’s probably OK for the meantime.

On the other end I’m just facing another problem on top of everything.

So basically for the view pages of my model I’m using a similar config:




            array(

                'CHttpCacheFilter + view, share',

                'cacheControl' => 'private; max-age=31536000',

                'etagSeedExpression' => "Yii::app()->getController()->action->id . \$_GET['id']",

                'lastModifiedExpression' => "Yii::app()->db->createCommand()->select('created')->from('doodle')->where('id=:id', array(':id'=>\$_GET['id']))->queryScalar()",

            ),



now, if the user tries to reach a model that has been deleted the lastModifiedException throws an error and breaks everything.

I had a look at your code and the most flexible solution would obviously be to add an additional parameter in order to throw the right exception or do something else in case the expression fails…

Probably the simplest is to have a default value to be used in case the returned expression is nothing harmful (as in my case is an empty string).

Got any ideas?

Yes. Quite frankly, you’ve made poor choices for your expressions. Action and model id are no good choices for etag seeds. You might even skip on that. ‘share’ sounds like an action that is being used to manipulate models rather than representing them. Caching such actions might not be a good idea. (on a related note: CHttpCacheFilter will only cache GET and HEAD requests anyway)

The field created implies it won’t be manipulated after the models record has been saved to the database. This makes it unsuitable for the lastModified value. Is there a modified field you could use instead?

As for the state of things: I’ve got a patch ready that sort-of hacks around the problem with session headers by setting a header via session_cache_limiter() and immediatly overwriting it wiht a custom value. This will need some testing as I suspect the behaviour of header() and session_cache_limiter() has changed between PHP v5.3 and 5.4.

Further patches in the pipe involve impoved interoperability with CCacheDependency (hashing CCacheDependency.getDependentData() is a bit faster than hashing the entire object) and making the controller available via $controller in the expressions (which should allow for more complex yet readable code).

A fix is ready.

Ok, the fix has just been merged :)