All Downloads are FREE. Search and download functionalities are using the official Maven repository.

templates.docs.httpCaching.html Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
{#==========================================
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.





© 2015 - 2024 Weber Informatics LLC | Privacy Policy