Caching Headers Plugin

2 votes

15% of Grails users

compile ":cache-headers:1.1.7"

 Documentation  Source  Issues


Improve your application performance with browser caching, with easy ways to set caching headers in controller responses


Install the plugin by adding a dependency in the plugins section of BuildConfig.groovy:

plugins {
   runtime ':cache-headers:<plugin_version>'


This plugin helps you improve your application performance with browser caching, with easy ways to set caching headers in controller responses, and elegant ways to deal with ETag and Last-Modified generation and checking.

You can use this plugin to prevent caching of pages (e.g. forms), specify long-term caching on infrequently changing content, and pass information to caching servers between the client and your app, and also to avoid regeneration of content if it has not changed since the client last downloaded it (even though the client may have an indication it has expired).

The methods provide a semantic approach to caching based on what you want to actually achieve, and does the "right thing" with caching headers to achieve it. This is awkward to achieve just by setting headers yourself, in terms of compatibility with older or totally lame browsers (no names needed...) behaving differently and requiring different headers to operate as expected. Doing it directly with headers leads to all kinds of fun with Expires/max age/cache control/last modified headers.

All these methods are available in controller actions. If you need to call them from another context, see the source of the CacheHeadersService where these methods reside

Commercial Support

Commercial support is available for this and other Grailsrocks plugins.


There are several controller dynamic methods added by the plugin, which you use to indicate how the current response should be cached.

cache(boolean canCache)

Using this method you can instantly prevent the current response being cached ever:

class MyController {
   def oneTimeInfo() {
       cache false

render "This is never cached" } }

This will set any response headers required to completely prevent caching. This is good for things like pages where the data used to generate them is no longer available (for example see or for stuff that must always use the latest live data.

Perhaps irritatingly to some, setting "cache true" is not permitted and will throw an exception. Just don't call it. You can't force caching, so "cache true" makes no sense. Strangely, "cache false" does make perfect sense. Language is a strange thing!

cache(Map arguments)

This form of the method gives access to precise control over the caching of the response, such as how long it is to be considered valid for, whether or not it can be cached by intermediary caching servers and reused for other users (i.e. has no private info in it) etc.

The arguments supported are:

  • store - set this to false to prevent caching servers between the client and your app from keeping a copy of the content. By default storing is permitted.
  • shared - set this to true to permit caching servers to serve this same content to other users. By default this is false.
  • validFor - set this specify how long the current response is valid for, in seconds. Sets all the headers required to achieve this cross-browser. Not compatible with validUntil. Use one or the other.
  • validUntil - set this to a Date instance if you have a specific end-date in mind for your content. Not compatible with validFor. Use one or the other.
  • neverExpires - set to true to force the client to never request a new copy of this content, unless the user forces it with a refresh in their client or the client cache is flushed. Not compatible with validFor or validUntil.
Here's an example of usage:

class ContentController {
   def show() {
       cache shared:true, validFor: 3600  // 1hr on content

def todaysNewItems() { cache shared: true, validUntil: new Date()+1 render(....) }

def searchResults() { cache validFor: 60 // don't re-run same search for 60s! render(....) }

def personalInfo() { cache store: false // don't let intermediate caches store this! (https:// would imply this) render(....) }

cache(String presetName)

This variant of the cache method allows you to define presets for your cache settings in Config.groovy and recall them by name.

This is much more convenient as you can clearly define and centralize your caching strategy, so that controllers only need to indicate what they are trying to achieve semantically:


cache.headers.presets = [
    authed_page: false, // No caching for logged in user
    content: [shared:true, validFor: 3600], // 1hr on content
    news: [shared: true, validUntil:new Date()+1],
    search_results: [validFor: 60, shared: true]


class ContentController {
   def show() {
       cache "content"

def todaysNewItems() { cache "news" render(....) }

def searchResults() { cache "search_results" render(....) }

def personalInfo() { cache "authed_page" render(....) }

This also makes it trivial to have per-environment caching settings so you can prevent / relax caching during development.


This method is a shortcut for setting the Last-Modified header of your response. Its important to get this as correct as you can for the content you are serving. It is used in several caching situations in browsers and proxies.

If you are not using the withCacheHeaders method (see next section) you can use this method to set the Last-Modified header explicitly:

class BookController {
   def show() {
       def book = Book.get(

lastModified book.dateUpdated

render(....) }

A Date or Long can be passed to the method, and it will be encoded as per the HTTP date format.

withCacheHeaders(Closure dsl)

This method acts similarly to the Grails withFormat method, but lets you provide code that will let the plugin automatically handle ETag-based "If-None-Match" and Last-Modified based "If-Modified-Since" requests for you.

This means that even if your content cannot be cached for long periods in the client, you can avoid the cost of re-processing and transmitting the same content if you can identify whether or not it has changed.

In this case the client sends a GET request, and your app automatically replies with a "304 Not Modified" response if your code indicates that the content the client has can still be used.

Here's an example:

class BookController {
  def show() {
     withCacheHeaders {
         def book = Book.get(

etag { "${book.ident()}:${book.version}" } lastModified { book.dateCreated ?: book.dateUpdated } generate { render(view:"bookDisplay", model:[item:book]) } } } }

There are three DSL methods you can implement.

The optional "etag" closure is executed if the code needs to generate an ETag for the current request. Even if the request does not include an "If-None-Match" header, this closure will be called if the content is generated, to set the header for clients that have not received it before.

The optional "lastModified" closure is executed to set the Last-Modified header, and to compare it with any "If-Modified-Since" header sent by clients.

In Grails 2.x, controllers actions are class methods instead of public closures, and that leads to a name clash between 'lastModified' the method, and 'lastModified' the internal DSL of the withCacheHeaders closure. A simple workaround is to use this syntax:
class BookController {
  def show () {
     withCacheHeaders {
         def book = Book.get(

delegate.lastModified { book.dateCreated ?: book.dateUpdated } generate { render(view:"bookDisplay", model:[item:book]) } } } }

If either the ETag or Last-Modified values fail requirements set by the request headers, the "generate" closure will be called to render the response. When this happens, the plugin will automatically set Last-Modified and ETag using the values your closures provided.


You use Config.groovy to control whether the caching plugin is used at all from config, so for example you can completely prevent all caching header operations during tests or development:

// Prevent any client side caching for now
cache.headers.enabled = false

You can also set up preset cache settings by name:

cache.headers.presets = [
    unauthed_page: [shared:true, validFor: 300], // 5 minute refresh window
    authed_page: false, // No caching for logged in user
    content: [shared:true, validFor: 3600], // 1hr on content
    recent_items_feed: [shared: true, validFor: 1800], // 30 minute throttle on RSS updates
    search_results: [validFor: 60, shared: true],
    taxonomy_results: [validFor: 60, shared: true]

To use presets, see the above description for the cache(String presetName) method variant.