Proper Cache Busting

And other goodies related to Apache configuration

Posted on: 220624

Recently I've had some difficulty getting scripts to reload once cached. A common method is to include a cache busting querystring to the URL of the script, but I find this common solution to be hackish and downright fugly to be honest. There's also the issue that the files are still cached on the user's device, just under a growing list of different resource names. This solution causes more harm than good. There must be a better way to stop scripts from being cached...

There's two parts to the solution here: the first being certain HTTP headers which prevent a file from being cached. I found these care of the wonderful resource provided by Mozilla: MDN: Cache-Control. The particular project in question has Javascript and CSS files which may occasionally be updated by the user, but most of the time will not be changing. For this particular case, I'm going to be using the Cache-Control: no-cache header. This states that the files may be cached, but must be validated to ensure new versions are not available. Other cases may vary.

Now providing this header along with the files served is a simple matter in PHP; you only have to use the header() function to set the Cache-Control and Content-Type headers before streaming the Javascript or CSS. That doesn't work for my case. In an attempt to mitigate server load caused by running tons of PHP scripts, the page loads these files directly. So we need another way to apply this header to the files being served without involving PHP. For the sake of anyone reading this who might be using PHP, this would be the code to achieve that end for a javascript file...

<?php $file = $_SERVER['QUERY_STRING']; $file = str_replace('../','',$file); header('Content-Type: text/javascript'); header('Cache-Control: no-cache'); if(substr($file,-3)!='.js' || !file_exists($file)){ echo '// invalid request!'; exit; } echo file_get_contents($file);

The sample prevents users from sequestering non-javascript files as well as preventing traversal attacks.

Apache .htaccess file

But there's a more elegant solution than that. One that speaks to the server directly. Here's an excerpt from the .htaccess file in the project, that directs the user to the location of the particular CSS and Javascript files that should not use the cached versions when updates are available...

RewriteCond %{DOCUMENT_ROOT}/nocache/$1.js -f RewriteRule ^nocache/(.+)\.js$ /nocache/$1.js [L] RewriteCond %{DOCUMENT_ROOT}/nocache/$1.css -f RewriteRule ^nocache/(.+)\.css$ /nocache/$1.css [L]

In this example, Javascript and CSS files which should not be cached are located in a nocache/ folder.

Using Apache header.c module

First we're going to need an Apache module enabled called headers. If you have terminal access to your server, use the following commands to enable the module, then restart Apache...

sudo a2enmod headers systemctl restart apache2

Enabling the headers module on

Set the header...

Header set Cache-Control no-cache RewriteCond %{DOCUMENT_ROOT}/nocache/$1.js -f RewriteRule ^nocache/(.+)\.js$ /nocache/$1.js [L] RewriteCond %{DOCUMENT_ROOT}/nocache/$1.css -f RewriteRule ^nocache/(.+)\.css$ /nocache/$1.css [L]

Without first enabling the headers module, the first line in that excerpt will cause a 500 error. The issue with this code is that the no caching will be set for all pages loaded. We don't want that. We're going to need a condition that handles only the files in the nocache/ directory. In Apache 2.4 or later we could use a generic <If> directive...

<If "%{REQUEST_URI} =~ m#^/nocache/#"> Header set Cache-Control no-cache </If> RewriteCond %{DOCUMENT_ROOT}/nocache/$1.js -f RewriteRule ^nocache/(.+)\.js$ /nocache/$1.js [L] RewriteCond %{DOCUMENT_ROOT}/nocache/$1.css -f RewriteRule ^nocache/(.+)\.css$ /nocache/$1.css [L]

For Apache 2.2, we would have to set an environment variable and use that as a condition...

SetEnvIf Request_URI ^/nocache/ NOT_CACHING Header set Cache-Control no-cache ENV=NOT_CACHING RewriteCond %{DOCUMENT_ROOT}/nocache/$1.js -f RewriteRule ^nocache/(.+)\.js$ /nocache/$1.js [L] RewriteCond %{DOCUMENT_ROOT}/nocache/$1.css -f RewriteRule ^nocache/(.+)\.css$ /nocache/$1.css [L]

And there you have it! Preventing caching of changing scripts and stylesheets without having to use a cache-busting querystring that floods the user with numerous cached copies of your files. Proper cache busting.

Mobile Browsers

Unfortunately mobile browsers do not honour these headers. Any web apps with updating files that target mobile devices would have to use the querystring method until Google, Mozilla, Microsoft and Apple get their heads out of their asses and respect headers on their mobile browsers. :(