templates.docs.httpCaching.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{#==========================================
Docs : "HTTP Caching"
==========================================#}
HTTP Caching
Spincast supports many HTTP caching features, as described in the
HTTP 1.1 specification :
-
Cache-Control
headers
-
Last modification dates
-
Etag
headers
-
"No Cache" headers
Finally, Spincast also provides a mechanism for Cache Busting.
Cache-Control
The Cache-Control
header (and the similar, but older, Expires
header) is used to
tell a client how much time (in seconds) it should use its cached copy of a resource
before asking the server for a fresh copy.
This Cache-Control
header can first be specified when you build a route (see
HTTP Caching route options), using the cache(...)
method.
For example :
router.GET("/test").cache(3600).handle(handler);
There are three options available when using this cache(...)
method :
-
The number of seconds the client should uses its cached version without
asking the server again.
-
Is the cache
public
(default) or private
. The private
option means an end client can cache the resource but not an intermediate proxy/CDN
(more information).
-
The number of seconds a proxy/CDN should use its cached version without
asking the server again. Only use this option if it must be different than the
number of seconds for regular end clients.
This cache(...)
method can also be used dynamically,
in a route handler, using the cacheHeaders()
add-on :
@Override
public void myHandler(AppRequestContext context) {
context.cacheHeaders().cache(3600);
context.response().sendPlainText("This will be cached for 3600 seconds!");
}
Default caching
When you do not explicitly set any caching options for a Static Resource
, some defaults are automatically used. Those
defaults are configurable using the SpincastConfig
class.
There are two variations for those Static Resources default caching configurations :
-
One for plain Static Resources. The default is to send headers so the resource
is cached for
86400
seconds (1 day).
-
One for Dynamic Resources. The default is to send headers so the resource
is cached for
3600
seconds (1 hour).
When a resource can be generated, it often means that the resource may change more frequently. This is why
dynamic resources have their own default caching configuration.
When a Static/Dynamic Resource is served, a Last-Modified
header is also automatically
sent. This means that even when the client does ask for a fresh copy of a resource, it will often
receive a "304 - Not Modified"
response and will therefore again use its
cached copy, without unnecessary data being transferred over the network.
Finally, note that no default caching headers are sent for regular Routes. You have to explictly use
cache(...)
to send some. But if you use cache()
as is, without any parameter, then
default values will be used (still configurable using the SpincastConfig
class).
No Cache
Even when no explicit caching headers are sent, some clients (browsers, proxies, etc.) may use a default
caching strategy for the resources they download. For example, if you press the "back" button,
many browsers will display a cached version of the previous page, without requesting
the server for a fresh copy... Even if no caching headers were sent for that page!
If you want to tell the client that it should disable any kind of caching for a resource,
you can use the noCache(...)
method. This can be done when building a Route :
router.GET("/test").noCache().handle(handler);
Or can be used dynamically, in a Route Handler
:
@Override
public void myHandler(AppRequestContext context) {
context.cacheHeaders().noCache();
context.response().sendPlainText("This will never be cached!");
}
Finally, note that you can not use the noCache()
options on a Static Resource
since
this would defeat the notion of a "static" resource.
Cache Busting
Cache busting is the process of adding a special token to the URL of a resource in a way that simply
by changing this token you invalidate any cache a client may have.
For example, let's say that in a HTML page you reference a .css
file that you want
the client to cache (since it won't frequently change) :
<link rel="stylesheet" href="/public/css/main.css">
The associated Route may be :
router.file("/public/css/main.css").cache(86400).classpath("/css/main.css").handle();
As you now know, when this resource is served a Cache-Control
header will
be sent to the client so it caches it for 24 hours. And this is great! But what happens if you release
a new version of your application? You may then have changed "main.css"
and you want all clients
to use the new, fresh, version. How can you do that if many clients already have the old version in cache
and you specified them that they can use that cached version for 24 hours? This is where cache busting
become handy!
To enable cache busting for a particular resource, you add the "cacheBuster"
template
variable to its URL. This template variable is provided by Spincast, you simply need to add
it in your HTML pages. For example, if you use Pebble,
which is the default
Templating Engine
provided with Spincast, you need to use
"{% verbatim %}{{cacheBuster}}{% endverbatim %}"
as the cache buster token.
We recommend that you add this token right before the file name of the resource :
{% verbatim %}
<link rel="stylesheet" href="/public/css/{{cacheBuster}}main.css">
{% endverbatim %}
When the HTML is rendered, the result will look something like this :
<link rel="stylesheet" href="/public/css/spincastcb_1469634250814_main.css">
When Spincast receives a request, it automatically removes any cache busters from the URL. So
-
"/public/css/spincastcb_1111111111111_main.css"
-
"/public/css/spincastcb_2222222222222_main.css"
will both result in the exact same URL, and will both target the same resource :
-
"/public/css/main.css"
But (and that's the trick!) the client will consider both URLs as different, targeting different resources, so
it won't use any cached version when a cache buster is changed!
By default, the cache busting code provided by Spincast will change every time the application is restarted. You can modify
this behavior and/or the format of the token by overriding the getCacheBusterCode()
and
removeCacheBusterCodes(...)
methods from
SpincastUtils.
Finally, note that the cache busting tokens are removed before the routing is done, so they don't affect it in any way.
Etag and Last Modification date
The Etag
and Last modification date
headers are two ways of validating if the cached version
a client already has of a resource is still valid or not. We will call them "Freshness headers"
.
The client, when it wants to retrieve/modify/delete a resource it already has a copy of, sends a request
for that resource by passing the Etags
and/or Last modification date
it currently has for that
resource. The Server validates those values with the current information of the resource and decides if the
current resource should be retrieved/modified/deleted. Some variations using those headers are even used to
validate if a client can create a new resource.
Note that freshness headers management is not required on all endpoints. For example, an endpoint that would compute
the sum of two numbers has no use for cache headers or for freshness validation! But when a REST
endpoint deal with
a resource, by Creating, Retrieving, Updating or Deleting it, then freshness validation is a must
to respect the HTTP specification.
Proper use of the freshness headers is not trivial
The most popular use for freshness headers is to return a 304 - Not Modified
response when a
client asks for a fresh copy of a resource but that resource has not changed. Doing so, the response
is very fast since no unnecessary data is actually transmitted over the network : the client simply continue to use its
cached copy.
This "304"
use case if often the only one managed by web frameworks. The reason is that it can
be automated, to some extent. A popular way of automating it is
to use an "after" filter to generate a hash from the body of a resource returned as the response to a
GET
request. This hash is used as an ETag
header and compared with any existing
If-None-Match
header sent by the client. If the ETag
matches, then the generated resource is
not sent, a "304 - Not Modified"
response is sent instead.
This approach may be attractive at first because it is very simple and doesn't require you to do anything
except registering a filter. The problem is that this HTTP caching management is very, very, limited
and only addresses one aspect of the caching mechanism described in the
HTTP specification.
First, it only addresses GET
requests. Its only purpose is to return a 304 - Not Modified
response instead of the actual resource on a GET
request. But, freshness headers should be used
for a lot more than that. For example :
-
If a request is received with an
"If-Match"
(Etag) or an "If-Unmodified-Since"
(Last modification date) header, and the resource has changed, then the request must fail
and a 412 - Precondition Failed
response must be
returned. (specification)
-
If a request with an
"If-None-Match: *"
header is received on a PUT
request, the resource must not be created if any version of it already exists.
(specification)
Also, to hash the body of the resource to create an ETag
may not always be
appropriate. First, the resource must be generated for this hash to be computed.
But maybe you shouldn't have let the request access the resource in the first place! Also, maybe there is
a far better way to generate a unique ETag
for your resource than to hash its body, using one of its
field for example. Finally, what happens if you need to "stream" that resource?
If you need to flush the response more than once when
serving it? In that case, the "after" filter wouldn't be able to hash the body properly and
send it as an ETag
header.
All this to say that Etags
and Last modification date
may seem easy to manage
at first, but in fact, they require some work from you. If you simply want to manage the GET
use case where a 304 - Not Modified
response can be returned instead of the resource itself,
then creating your own "after" filter should be quite easy (we may even provide one in a future release). But if you want
your REST
endpoints to be more compliant with the HTTP specification, then keep reading
to learn how to use the cacheHeaders()
add-on and its validate()
method!
Freshness management using the cacheHeaders() add-on
There are three methods on the cacheHeaders()
add-on made to deal with freshness headers in a
Route Handler
:
-
.etag(...)
-
.lastModified(...)
-
.validate(...)
The
first two are used to set the Etag
and Last modification date
headers
for the current version of the resource. For example :
// Set the ETag
context.cacheHeaders().eTag(resourceEtag);
// Set the Last modification date
context.cacheHeaders().lastModified(resourceModificationDate);
But setting those freshness headers doesn't make sense if you do not validate them when they are sent
back by a client! This is what the .validate(...)
method is made for. It validates the current
ETag
and/or Last modification date
of the resource to the ones sent by the client.
Here's an example of what using this method looks like :
if(context.cacheHeaders().eTag(resourceEtag)
.lastModified(resourceModificationDate)
.validate(resource != null)) {
return;
}
We will look in details how to use the validate()
method, but the important thing to remember is
that if this method returns true
, then your route handler should return immediately,
without returning/modifying/creating
or deleting the associated resource. It also
means that the response to return to the client has already been set and should be returned as is : you
don't have to do anything more.
Note that, in general, you will use ETags
or Last modification dates
,
not both. Since ETags
are more generic (you can even use a modification date as an ETag
!), our following
example will only focus on ETags
. But using the validate(...)
method is the same, that you use
ETags
or Last modification dates
.
Using the "validate(...)" method
Let's first repeat that, as we previously said,
the Static Resources have their
Last modification date
automatically managed. The Server simply validates the modification
date of the resource's file on disk and use this information to decide if a new copy of the resource should be
returned or not. In other words, the validation pattern we are going to explain here only concerns regular Routes, where
Route Handlers
manage the target resources.
The freshness validation pattern looks like this, in a Route Handler
:
public void myRouteHandler(AppRequestContext context) {
// 1. Gets the current resource, if any
MyResource resource = getMyResource();
String resourceEtag = computeEtag(resource);
// 2. Sets the current ETag and validates the freshness of the
// headers sent by the client
if(context.cacheHeaders().eTag(resourceEtag)
.validate(resource != null)) {
return;
}
// 3. Validation done!
// Now the core of the handler can run :
// it can create/return/update/delete the resource.
// ...
};
Explanation :
-
4 : We get the actual resource (from a
service for example). Note that
it can be
null
if it doesn't exist or if it has been deleted.
-
5 : We compute the
ETag
for the resource. The ETag
can be anything : it is specific to your application how to generate it.
Note that the ETag
can be null
if the resource doesn't exist!
-
9 : By using the
cacheHeaders()
add-on, we set the ETag
of the current resource. This will add the appropriate headers to the response :
those headers will be sent, whatever the result of the validate(...)
method is.
-
10 : We call the
validate(...)
method.
This method takes one parameter : a boolean indicating if the resource currently exists
or not. The method will validate the current ETag
and/or the Last modification date
with the ones received by the client. If any HTTP freshness rule is matched, some appropriate headers
are set in the response ("304 - Not Modified"
or "412 - Precondition Failed"
)
and true
is returned. If no freshness rule is matched, false
is returned.
-
11 : If the
validate(...)
method returns
true
, our Route Handler
should return immediately, without further processing!
-
14-18 : If the
validate(...)
method returns
false
, the main part of our Route Handler
can run, as usual.
As you can see, the validation pattern consists in comparing the ETag
and/or
Last modification date
of the actual resource to the headers
sent by the client. A lot of validations are done in that validate(...)
method,
we try to follow as much as possible the full HTTP specification.
Note that if the resource doesn't currently exist, you should not create it before calling
the validate(...)
method! You should instead pass false
to the validate(...)
method.
If the request is a PUT
asking to create the resource,
this creation can be done after the cache headers validation, and only if the validate(false)
method returns false
. In that case, the ETag
and/or Last modification date
will have to be added to the response by calling eTag(...)
and/or lastModified(...)
again :
public void createHandler(AppRequestContext context) {
// Let's say the resource is "null" here.
MyResource resource = getMyResource();
// The ETag will be "null" too.
String resourceEtag = computeEtag(resource);
if(context.cacheHeaders().eTag(resourceEtag)
.validate(resource != null)) {
return;
}
// The validation returned "false" so we can
// create the resource!
resource = createResourceUsingInforFromTheRequest(context);
// We add the new ETag to the response.
resourceEtag = computeEtag(resource);
context.cacheHeaders().eTag(resourceEtag);
}
If the resource doesn't exist and the request is a GET
, you can return
a 404 - Not Found
after the freshness validation. In fact, once the
validation is done, your handler can be processed as usual, as if there was no prior validation...
For example :
public void getHandler(AppRequestContext context) {
MyResource resource = getMyResource();
String resourceEtag = computeEtag(resource);
if(context.cacheHeaders().eTag(resourceEtag)
.validate(resource != null)) {
return;
}
if(resource == null) {
throw new NotFoundException();
}
return context.response().sendJson(resource);
}
To conclude, you may now see that proper management of HTTP freshness headers
sadly can't be fully automated. A Filter
is simply not enough!
But, by using the
cacheHeaders()
add-on and its validate(...)
method, a
REST
endpoint can follow the HTTP specification and be very
performant. Remember that not all endpoints require that freshness validation, though! You can
start your application without any freshness validation at all and add it on endpoints
where it makes sense.