Client Side Caching with Grails & ETags
Grails & ETags
Currently Grails
(1.0-RC4 at time of writing) does not serve ETags
with images. HTTP/1.1 compliant web-servers and clients can use the ETag mechanism to prevent resources from being re-downloaded, reducing network traffic and (hopefully!) server load. Here's a quick and simple way to introduce them and hopefully cut down on your web applications' outgoing traffic - note that this example is simplistic and does not cater for images that will change while remaining on the same URI.
Quick Overview
We'd like each image response that Grails returns to contain an ETag header - this value will be generated server-side by simply base64 encoding the request URI. A HTTP/1.1 compliant client will cache these images with their respective ETag values upon page load. When the client re-requests an image it will send an 'If-None-Match' header containing our ETag value - if we're happy with it server-side we will respond with a simple & small (~200B) HTTP 304 rather than delivering the full image content back to the client again.Details
Initially I had hoped to get away with a simple Grails filter
to do the job but unfortunately filters do not get invoked for 'direct-mapping' requests
such as images. As a result a few pieces are necessary to complete the puzzle:
- Grails Filter - to determine whether to send a HTTP 304 Not-Modified or 200 response
- Images Controller - to return the image data to the client
- UrlMappings addition - to route image requests to the Images Controller
Filter
Create a file WebFilters.groovy in your grails-app/conf directory with the following content:
class WebFilters {
def filters = {
cacheImage(controller:'images') {
before = {
boolean serveImage = true
String browserEtag = request.getHeader('If-None-Match')
def requestURI = request.'javax.servlet.forward.request_uri'
String serverEtag = requestURI.bytes.encodeBase64()
if(browserEtag == serverEtag) {
serveImage = false
response.sendError(304)
}
response.addHeader('ETag', serverEtag)
return serveImage
}
}
}
}
As you can see from the code above the Groovy JDK adds a convenient encodeBase64() method to Byte. The serveImage boolean lets the filter chain know whether or not it should to proceed to the next filter.
Controller
Now add a controller called 'Images' (i.e. grails-app/controllers/ImagesController.groovy) either via grails create-controller Images or by directly creating the file. Add the following content:
import javax.activation.FileTypeMap
class ImagesController {
def index = {
def imageName = "${params.image}.${request.format}"
def contentType = FileTypeMap.defaultFileTypeMap.getContentType(imageName)
def imageFile = new File("web-app/images/${imageName}")
if(imageFile.exists()) {
def bytes = imageFile.readBytes()
response.contentType = contentType
response.outputStream << bytes
} else {
response.sendError(404)
}
}
}
This simplistic controller uses the relative web-app/images path as the base directory to serve images - you may want to externalize this into your configuration. Once again the Groovy JDK comes to the rescue with the File.readBytes() method. Also note that the grails request contains a 'format' attribute - this will contain the image format (jpg, gif etc.).
UrlMappings
Finally update your UrlMappings (grails-app/conf/UrlMappings.groovy) to route image requests into the newly created Images controller:
class UrlMappings {
static mappings = {
"/images/${image}" {
controller = 'images'
}
... rest of mappings go here
}
Conclusion
As you can see this is a very simple implementation which may or may not be suitable for your setup. You may want to include more factors in your ETag generation (e.g. last modified date) and externalize your resource location from web-app/images to some configuration property, but I hope that someone finds this useful :) Furthermore you could expand the scope to include javascript, css and other static resources. If you've got any comment/question/rant/other please ping me on [gus] at [energizedwork] dot [com].Add new attachment
Only authenticated users are allowed to upload new attachments.