Supporting Cache Controls in node.js
I am a big fan of Node.js and am happy to be seeing more instances across the internet. However a trend I have noticed is a misuse or lack of cache control headers, so I am going to show you a quick sample of how to enable support for ETag's and the If-None-Match Cache Validation Method.
Before we get into the code let's first make sure we understand the ETag and If-None-Match HTTP Request and Response Headers.
The ETag Header is part of the HTTP protocol and it is one of several mechanisms that HTTP provides for web cache validation, and allows a client to make conditional requests. This allows caches to be more efficient, and saves bandwidth, as a web server does not need to send a full response if the content has not changed.
The If-None-Match Header is sent by clients and is the last ETag they received for the object. If the file has not changed the ETag Response and If-None-Matched Request Header will match allowing the server can respond with a 304 Not Modified containing no payload and the requested object will be loaded from cache. This reduces server resources as you are not spending Disk or Network IO to read and send the response and bandwidth as the response is very small compared to returning an object.
So now that we know and understand these values what do they mean to you and how can you use them?
var http = require('http'), fs = require('fs'); http.createServer(function (request, response) { fs.readFile('.' + request.url, function (err, data) { if (err) { if (err.errno === 34) { response.statusCode = 404; } else { response.statusCode = 500; } response.end() } else { fs.stat('.' + request.url, function (err, stat) { if (err) { response.statusCode = 500; response.end() } else { etag = stat.size + '-' + Date.parse(stat.mtime); response.setHeader('Last-Modified', stat.mtime); if (request.headers['if-none-match'] === etag) { response.statusCode = 304; response.end(); } else { response.setHeader('Content-Length', data.length); response.setHeader('ETag', etag); response.statusCode = 200; response.end(data); } } }) } }) }).listen(8080);
Now lets take the above example and examine the parts related to ETag Support
fs.readFile('.' + request.url, function(err,data) { if(err) { if(err.errno === 34) { response.statusCode = 404; } else { response.statusCode = 500; } response.end(); }
So here we read the file the user requested and return a 404 File not Found error if the file is not found, a 500 Internal Server Error if an error occurs reading the file, or assign the contents of the file to the variable data.
fs.stat('.' + request.url, function(err,stat) { if(err) { response.statusCode = 500; response.end()
This next part is where we start to build our ETag, using fs.stat we gather some data about our requested file. If an error occurs reading the file information we return a 500 Internal Server Error to the end user
etag = stat.size + '-' + Date.parse(stat.mtime); response.setHeader('Last-Modified', stat.mtime); if(request.headers['if-none-match'] === etag) { response.statusCode = 304; response.end(); } else { response.setHeader('Content-Length', data.length); response.setHeader('ETag', etag); response.statusCode = 200; response.end(data); }
Ok so now that we are done with all of our basic error handling lets get to the actual part that pertains to handling the request and responding to the client. We accomplish this using the stat object returned from the fs.stats call from above.
The first thing we want to do is create the etag, typically this is crafted using inode details, file size, and file modified time. This however is not the best solution as if you have a cluster of servers the inode will be different across machines so we are going to take that out of the equation here and only use file size and last modified times.
Once we have our etag data for the requested file we set the common value Last-Modified which will be used by both response types.
Now we check if the request contains the header If-None-Modified and if it matches our etag. If they do match we response with a 304 Not Modified and that is it, if they do not match we set the Content-Length and ETag Headers as well as the HTTP Status Code 200 and the requested file contents as the payload.
Now let's do some testing. Grab a copy of the script above and create a test file such as test.css, start up the example script and open your browser to http://localhost:8080/test.css
If you are using Google Chrome or Firefox open up your Network Tab in the Developer Tools and notice the first request returned a status code of 200 and the contents of your file. Now reload the page, you should get back a 304 Not Modified Response and if you look at the bytes transferred you will notice this was much smaller than your first request.
Remember just because you use a reverse proxy in front of your Node.js application does not mean you are sending these headers to your clients. Do some testing on your own to see the difference in performance and server resources once you enable support for ETag's in your applications
The sample provided does not cover all of your error handling, security or performance, it is meant as an example only and should be secured before using in a production environment.
