The Blueprint
I spent more than half a day yesterday trying to uncover why the code that delivers our Javascript and CSS suddenly quit being cached locally in the browser. Seemingly out of the blue, every page load was pulling down a new copy of the files, which is unacceptable when they are over 100kb even in a compressed state. Akira uses a variation of the Combine script to combine, compress, serve and cache all our JS and CSS. It’s a script that more people should be using due to it’s simplicity and how well it works… when it works. Let’s take a step backwards and I’ll tell you how the caching works before I get into how I fixed it, why it was broken and why PHP is to blame (sorta).
Etags
An Etag is an HTTP response header that may be returned from your web server and is used to determine change in content on a given page. What that means is that it allows you to check a page and return either a 304 Not Modified response with no content or a 200 OK response with all the page content. Or even more practically, we can check our CSS and JS pages and the server will tell us if they’ve changed or not… and if not then use the local browser cache.
You can’t just call http://example.com/style.css and have it work. You have to have PHP check and send the proper Etag headers, which is where the Combine script comes in since it does all this for you. The script checks the Last Modified Date of each CSS/JS file and uses that in the Etag. The Etag changes when the CSS file changes, thereby telling your browser to use the new content. It’s brilliant really… and I can’t figure out why more people don’t use this method other than the VERY MINIMAL overhead (low milliseconds) that is needed to check the Etag header.
Etags and PHP
Lets look at how PHP gets involved in all this. PHP has access to a server variable called $_SERVER['HTTP_IF_NONE_MATCH'] that returns NULL if the Etags do not match. Using this variable it allows us to know if any content has changed and then act on that. Let me give you a quick example of how it works:
header('Etag: "This is the Etag header content."');
if(!isset($_SERVER['HTTP_IF_NONE_MATCH'])){
echo "$_SERVER['HTTP_IF_NONE_MATCH'] is not set";
}else{
// $_SERVER['HTTP_IF_NONE_MATCH'] is set so return 304
header ("HTTP/1.0 304 Not Modified");
header ('Content-Length: 0');
}
Houston we have a PHP problem
So if the Combine script works so great why did it stop working for us? In simple terms, PHP sort of got in the way. We started off using Combine in it’s native form… just a straight call to combine.php which served the CSS/JS. However, we recently integrated it into our framework to keep things streamlined, but I never went back to check the caching. I just figured it would still work. That was my mistake and why several months later I thought it quit working “out of the blue” when in fact it had stopped as soon as we integrated it.
The root problem is that when PHP uses sessions it modifies the cache headers because of the dynamic nature of using sessions. And wouldn’t you know it.. we use sessions (surprise surprise) and our app calls in a sessions class in the overhead portion of the app. What that means is that a session was started on each page load, including these JS and CSS calls. What happens is that when session_start() is called the server variable $_SERVER['HTTP_IF_NONE_MATCH'] is removed since PHP thinks that we don’t want to serve static content. Fun times and I now see why Rasmus dislikes frameworks due to the overhead issues.
Solution
Not being a core PHP dev I can only assume there is a real reason for how PHP handles sessions that is helpful in other instances. However to get around it I can either bypass the session_start() for JS and CSS calls (changing the overhead of the framework) or I can use the following helpful bit of code that is in PHP to allow us to change the normal behavior of sessions. Before we call session_start() we simply tell PHP that we want to set the “session cache limiter” to private by calling session_cache_limiter(‘private_no_expire’).
So there you go.. a simple fix which took my half a day to track down. Hopefully I will have saved a few people the same headache when trying to use Etags and PHP sessions.
Search The Blog
Code & Projects
Categories
Archives
- October 2009
- September 2009
- August 2009
- June 2009
- May 2009
- April 2009
- February 2009
- December 2008
- November 2008
- October 2008
- September 2008
- August 2008
- April 2008
- March 2008
- February 2008
- July 2007
Thank you.
This save me alot of time :D
and works great with php 5.2.10 & apache 2.2.10.
Thanks!! Saved me ages! Works a treat, I was wondering why my Etags were being ignored