Summary
Improve your application performance with browser caching, with easy ways to set caching headers in controller responses
Installation
Install the plugin by adding a dependency in build.gradle
compile 'or.grails.plugins:cache-headers:2.0.1'
Description
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.
Usage
Cache - Boolean
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 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
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:
key | defaultValue | compatibility | description |
---|---|---|---|
|
true |
set this to false to prevent caching servers between the client and your app from keeping a copy of the content. |
|
|
false |
set this to true to permit caching servers to serve this same content to other users. |
|
|
Not compatible with validUntil. Use one or the other. |
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 validFor. Use one or the other. |
set this to a Date instance if you have a specific end-date in mind for your content. |
|
|
Not compatible with validFor or validUntil. |
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. |
Here’s an example of usage:
class ContentController {
def show() {
cache shared:true, validFor: 3600 // 1hr on content
render(....)
}
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 - PresetName
cache(String presetName)
This variant of the cache method allows you to define presets for your cache settings in application.yml
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
content:
shared: true
validFor: 3600
search_results:
shared: true
validFor: 60
or in application.groovy
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"
render(....)
}
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.
Last-Modified
lastModified(dateOrLong)
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(params.id)
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
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(params.id)
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.
Since 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(params.id)
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.
Configuration
You use application.yml
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.