ETags and Browser Cache


Caching resources on the browser is a key aspect to minimize redundant trips to the server and reduce the load on it. This works well for data that is static. But, there are cases where the data changes every once in a while (although less frequently) and you would want the browser to get the latest data without waiting for the cached data to expire. e.g. consider a scenario where a new version of the application is deployed with changes to css and javascript and you want the user to experience these changes without forcing them to manually refresh their cache. One option you might consider is to make these resources non-cacheable and compromise the page performance a bit. Entity tags( or ETags for short) help you cache such resources, without the performance compromise.

ETag is an identifier that represents the response content of a request. By comparing ETags, you should be able to tell if the response for a request has changed or not, and decide if you need to send the new data back to the client or just use the cached data.

HTTP supports ETag response headers which can be used along with If-None-Match request headers for conditionally managing the data cache. These headers are meant to be used for GET and HEAD requests, which are the recommended methods for fetching cacheable resources.

The following sequence diagrams illustrate the ETag workflow for an example of a 3-step resource fetch. Each step requests for the same resource.





Step 1

  • The Browser sends a request for a resource
  • The Server creates an ETag value for the resource using a hash or other techniques.
  • The Server sets this value in "ETag" header in the response.
  • The Server sends the resource along with ETag.
  • The Client caches the resource and the ETag

Step 2

  • The Client requests for the same resource
  • The Client sends the ETag value for the resource through the If-None-Match header.
  • The Server calculates the ETag for the resource.
  • The Server compares the new ETag with If-None-Match. 
  • If it matches, the server sends a 304 response code.
  • The Client uses the data from the cache.

Step 3

  • The Client requests for the same resource.
  • The Client sends the ETag value for the resource through the If-None-Match header.
  • The Server calculates the ETag for the resource.
  • The Server compares the new ETag with If-None-Match.
  • If it does not match, the server sets the ETag response header with the new value.
  • The Server sends the resource along with ETag.
  • The Client caches the new resource and the ETag


ETag generation


ETag generation in itself could add some amount of latency, depending upon the techniques used. The simplest technique to calculate is to generate a hash of the content with an algorithm like MD5. This can add additional latency especially with larger resources.

The generated ETag need not necessarily be unique, as long as it can indicate the change in the content. For example, in one of my projects, I used the content version as the ETag. The version indicated a change in the content and that was good enough for me to refresh the cache.

Spring support for ETags


WebRequest


org.springframework.web.context.request.WebRequest wraps around the HTTP request and response objects and provides convenient utilities to compare ETags.

public boolean ServletWebRequest.checkNotModified(String etag)

This function compares the ETag and If-Not-Match headers and sets the response code or the ETag response header. You can get hold of the WebRequest instance through the Controller handlers.

Consider the following example for a better understanding.

@RequestMapping(value="codelooru/getMyResource.do", method = RequestMethod.GET)
@ResponseBody
public MyResource getMyResource(HttpServletRequest request, WebRequest webRequest)
{
   MyResource myResource = myService.getMyResource();
   String etag = generateHash(myResource);
   boolean notModified = webRequest.checkNotModified(eTag);
   if (! notModified) {
      //Forces revalidation of cache. Browser would send an If-Not-Match with ETag in the next request.
      response.setHeader("Cache-control", "max-age=0");
      return myResource;
   }
   else {
      return null;
   }
}

ShallowEtagHeaderFilter


With WebRequest, you would be responsible for generating the ETag. If you wish the framework to do that for you automatically, then you can use ShallowEtagHeaderFilter. It generates ETag and also sets the relevant response header values. It uses MD5 to generate the hash and can be setup via web.xml. Of course, you can always sub-class it to implement a custom ETag generation.
<filter>
   <filter-name>shallowEtagHeaderFilter</filter-name>
   <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>shallowEtagHeaderFilter</filter-name>
   <url-pattern>/codelooru/*</url-pattern>
</filter-mapping>
 

Last-Modified and If-Modified-Since


An alternate approach to ETags is using the last modified time to check for data changes. These functions behave exactly the same way as ETag and If-Not-Match. Last-Modified is analogous to ETag and If-Modified-Since is to If-Not-Match. Again, it is useful if you can resolve the last modified time easily.

The following method in WebRequest can be used for this.


public boolean checkNotModified(long lastModifiedTimestamp)


Final Disclaimer


ETags are based on response content, which means that you cannot avoid a trip to the server to compare the content. What you are saving, however, is some bandwidth on the way back to client.




No comments:

Post a Comment