0x29

Nov 13, 2009

0x29 - nginx: Adding expiration headers

Today I was confronted with the tas of setting a proper expiration header to asset files. As a reminder: this is the aim:

  • when requesting an existing file with an additional timestamp parameter (i.e. "?123") add an expiration header.
  • when requesting an existing file without an additional timestamp parameter don't add an expiration header.
  • when requesting a non-existing file pass on the request to the passenger application.

After surfing the web for ages I found no working solution. In any case, the following did the trick. Read below for an explanation and how I checked this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Server settings. server_name and path should match.
server {
  listen 80;
  server_name server.dev;
  root /Users/me/sites/server/public;
  index index.html index.htm;

  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   html;
  }

  sendfile on;

  location / {
    if (-f $request_filename) { 
        set $file X;
    }

    if ($args ~* ^[0-9]+$) {
        set $file "${file}C";
    }

    # existing and can be kept by the client?
    if ($file = "XC") {
        expires 365d;    
    }

    # Doesn't exist? Go to rails app
    passenger_enabled on;
    rails_env production;
  }
}

This takes the following into account:

  1. The location value doesn't include the request query parameter
  2. This solution adds expiration headers to all requests where a file exists, not only to the "classical" asset requests.
  3. For some reason that I don't understand I had to put the "passenger_enabled" and "rails_env" inside the location block. Otherwise the rails application would never pick up any request with a file extension.

After I was done I found an apparently working configuration with a different (and I must say more elegant) approach in the comment section over at Craig's. It is sad you find the good stuff only after banging your head for hours :)

And this is how I tested it:

  1. Get the robots.txt file, it must not contain an "Expires" header:

    1
    2
    3
    4
    
     ~/site> curl -I http://server.dev/robots.txt
     HTTP/1.1 200 OK
     Server: nginx/0.7.61
     ...
    
  2. Get the robots.txt file, with a time stamp; it must contain an "Expires" header:

    1
    2
    3
    4
    5
    6
    7
    
     ~/site> curl -I http://server.dev/robots.txt?1234
     HTTP/1.1 200 OK
     Server: nginx/0.7.61
     Date: Fri, 13 Nov 2009 00:01:23 GMT
     Last-Modified: Sat, 08 Aug 2009 20:28:57 GMT
     Expires: Sat, 13 Nov 2010 00:01:23 GMT
     ...
    
  3. Get a dynamically created javascript file; it must not contain an "Expires" header, but must refer to Phusion Passenger in the "Server" header, and this regardless of whether or not a timestamp is set:

    1
    2
    3
    4
    5
    6
    7
    
      ~/sites/whispler> curl -I http://server.dev/xx.js?1234
      ...
      Server: nginx/0.7.61 + Phusion Passenger 2.2.5 (mod_rails/mod_rack)
    
      ~/sites/whispler> curl -I http://server.dev/xx.js
      ...
      Server: nginx/0.7.61 + Phusion Passenger 2.2.5 (mod_rails/mod_rack)