Redline Software Inc. - Winnipeg's Leader in Ruby on Rails Development

Nginx Expiry for Ruby on Rails

Many examples I see for setting the expiry header with Nginx look something like this…

1
2
3
4
5
6
location ~* (stylesheets|javascripts|images) {
  if (-f $request_filename) {
    expires max;
    break;
  }
}

This example runs a regex against the location of the incoming query for a path that contains one of the rails asset types. If the requested file exists then the expiry header is set. max here just tells the server to set the expiry date to the maximum value possible. You can set it to 10 years or any number of days if you prefer.

The issue with this is approach is that it is a “catch all” and ignores the query string on an asset if one exists. Both /stylesheets/styles.css?1234567890 and /stylesheets/styles.css are treated the same using this method of expiry.

If an asset is retrieved from the server (query string or not) and the expires header is set, the next time the file is referenced in the browser, the file will be loaded from the browser cache instead of the server. Now if the asset is modified on the server, rails will update the query string and the browser will recognize the difference and re-fetch it from the server. For the assets that don’t have an associated query string (like /stylesheets/styles.css), the browser will have no idea anything has changed and still load the file from it’s cache. This can cause styles or scripts to go out of sync between the server and the browser causing some nasty issues for the user.

So essentially, we only want to set the expires header on the assets that have a query string and not set the expires header on assets that don’t have one. This way the browser will always retrieve the most up to date version of a file.

What we’re looking for are 2 conditions that need to be satisfied before we set the expires header:

  1. The file is one of the asset types
  2. The query string contains the 10 digit timestamp

So let’s update the config to only set the expires header on asset files that have the 10 digit query string.

1
2
3
4
5
6
7
8
9
location ~* (stylesheets|javascripts|images) {
  if (!-f $request_filename) {
    break;
  }
  if ($query_string ~* "^[0-9]{10}$") {
    expires max;
    break;
  }
}

Here we use the same regex as before to find one of the asset types, but now instead of checking if the file exists, we check if it doesn’t exist. If the file doesn’t exist, we break out and don’t set the expires header which is fine. If the file does exist, then we don’t break, but continue evaluating our conditions because we’ve now satisfied condition 1 as we are looking at one of our asset files. The next thing we do is check the query string for a 10 digit pattern. If the check is successful then we’ve also satisfied condition 2 and can set the expires header for the file.

Now when we update an asset file on the server and Rails updates the query string, the server will re-fetch the file and set a new expires value… perfect.

If you want to check the file extension instead of their directories, you can use the following configuration instead. Note the location regex change.

1
2
3
4
5
6
7
8
9
location ~* \.(js|css|jpg|jpeg|gif|png)$ {
  if (!-f $request_filename) {
    break;
  }
  if ($query_string ~* "^[0-9]{10}$") {
    expires      max;
    break;
  }
}

Comments