Spring MVC - How to set 'ETag' and 'If-None-Match' headers?

[Updated: Sep 10, 2017, Created: Feb 4, 2017]

In this tutorial we will learn Spring support for 'ETag' and 'If-None-Match' headers. Check out the general step-by-step usage of these headers. It is also recommend to check out the related servlet tutorial to have a good understanding of setting these headers at low level.

If you have read our tutorial of setting 'Last-Modified' and 'If-Modified-Since', then you will find this tutorial very similar, that's because the both approaches ultimately achieve the same goals by using very similar API. ETag is considered more generic way to utilize client side cache then Last-Modified header.


Spring provides following ways to work with these headers.

Using WebRequest#checkNotModified()

Following methods of WebRequest transparently checks the value of 'If-None-Match' conditional request header and sets 'ETag' value in the response header as needed.

boolean checkNotModified(String etag)
boolean checkNotModified(String etag,
                         long lastModifiedTimestamp)

The second method allows to work with both 'ETag' and 'Last-Modified' approaches at the same time.

We can access WebRequest object or it's implementation ServletWebRequest by having them as a parameter of the @RequestMapping methods.


Returning ResponseEntity<T> after setting ETag value

Using following method of ResponseEntity.BodyBuilder will set the response 'ETag' header.

B eTag(String eTag)

When corresponding ResponseEntity is returned from the handler method, the header 'ETag' will be populated, and also the response will be converted to an HTTP 304 Not Modified with an empty body if the conditional header 'If-None-Match' sent by the client is same as the current ETag value of the resource. Obviously this approach does not save controller processing cycles, because the full response must be computed for each request, but it still saves bandwidth by not sending full body response to the client. If interested check out methods handleReturnValue() and isResourceNotModified() of HttpEntityMethodProcessor.java



Example

The Controller

@Controller
public class TheController {

  @RequestMapping(value = "/test1")
  public String handle1 (ServletWebRequest swr) {

      if (swr.checkNotModified(getETag())) {
          //it will return 304 with empty body
          return null;
      }
      return "myView";
  }

  @ResponseBody
  @RequestMapping(value = "/test2")
  public String handle2 (WebRequest swr) {

      if (swr.checkNotModified(getETag())) {
          return null;
      }

      String testBody = "<p>Response time: " + LocalDateTime.now() +
                "</p><a href='test2'>test2</a>";

      return testBody;
  }

  @ResponseBody
  @RequestMapping(value = "/test3")
  public ResponseEntity<String> handle3 (WebRequest swr) {

      String version = getETag();

      String testBody = "<p>Response time: " + LocalDateTime.now() +
                "</p><a href='test3'>test3</a>";
      return ResponseEntity
                .ok()
                .eTag(version)
                .body(testBody);
  }
  
  public String getETag () {
      return "version1";
  }
}

src/main/webapp/WEB-INF/pages/myView.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ page import="java.time.LocalDateTime"%>
<html>
  <body style="margin:20px;">
  JSP page
  <p>
  Date page created:<br/> <%= LocalDateTime.now()%>
  </p>
<a href='test1'>test1</a>
  </body>
</html>

src/main/resources/application.properties

spring.mvc.view.prefix= /WEB-INF/pages/
spring.mvc.view.suffix= .jsp

A Filter to log request and response headers

@WebFilter(urlPatterns = "/*")
public class HeaderLogger implements Filter {

  @Override
  public void init (FilterConfig filterConfig) throws ServletException {
  }

  @Override
  public void doFilter (ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {
      HttpServletRequest req = (HttpServletRequest) request;
      HttpServletResponse rep = (HttpServletResponse) response;
      System.out.println("----- Request ---------");
      Collections.list(req.getHeaderNames())
                 .forEach(n -> System.out.println(n + ": " + req.getHeader(n)));

      chain.doFilter(request, response);

      System.out.println("----- response ---------");
      rep.getHeaderNames()
         .forEach(n -> System.out.println(n + ": " + rep.getHeader(n)));

      System.out.println("response status: " + rep.getStatus());
  }

  @Override
  public void destroy () {
  }
}

The boot main class

@ServletComponentScan
@SpringBootApplication
public class Main {

  public static void main (String[] args) {
      SpringApplication sa = new SpringApplication(Main.class);
      sa.setLogStartupInfo(false);
      sa.setBannerMode(Banner.Mode.OFF);
      sa.run(args);
  }
}

Output

/test1

Console output on the first access:

----- Request ---------
host: localhost:8080
connection: keep-alive
pragma: no-cache
cache-control: no-cache
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=61D4E1694DBB534C596B49D64334AAAF
----- response ---------
ETag: "version1"
response status: 200

Note that we will have the same output on Ctrl+F5 because that ignores the browser cache.


On hitting F5 key (or reload button), a request will be sent to the server but it will return 304 with empty body unless the resource has been modified with a new ETag on server:

----- Request ---------
host: localhost:8080
connection: keep-alive
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
referer: http://localhost:8080/test1
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=61D4E1694DBB534C596B49D64334AAAF
if-none-match: "version1"
----- response ---------
ETag: "version1"
response status: 304

The page will still show the same content as browser uses the cached copy of the resource.

Also clicking on the link 'test1' will send the request to the server with conditional 'If-Non-Match' header to check the validity of the resource version. This is opposite to the last tutorial as 'Last-Modified' approach needs extra directive of 'no-cache' to do that.

Now change the ETag value returning from TheController#getETag() method and restart the web application (if running in debug mode then we can hot-swap the changes without restarting). On hitting F5 key in the browser with same uri /test1:

----- Request ---------
host: localhost:8080
connection: keep-alive
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
referer: http://localhost:8080/test1
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8
cookie: JSESSIONID=61D4E1694DBB534C596B49D64334AAAF
if-none-match: "version1"
----- response ---------
ETag: "version2"
Set-Cookie: JSESSIONID=E83387A726326DF73AF5BB995DD6F8FA;path=/;HttpOnly
response status: 200

The date displayed on the page will also change.

/test2

The outcome will be the similar on the same actions we used for /test1.

/test3

It will also produce the same outcome as well.

Note that for /test3, the corresponding handler method returns ResponseEntity after setting etag header. This approach is more transparent as we don't have to check the ETag validity manually by using WebRequest#checkNotModified() but there's no way to avoid creating the full response in the handler every time, so this approach should be avoided if creating response body is very expensive process.


No implicit support of ETag for static pages

In case of static resources, there's no implicit support of 'ETag' on the servlet container level. 'Last-Modified' response header support is always active for static pages, in that case the File object's lastModified date is used. For ETag, we can perhaps create a custom filter to intercept static pages and generate a hash code based on the content of the page which can be used as ETag header value. Spring provides such a filter out of the box, ShallowEtagHeaderFilter. We will see an example of ShallowEtagHeaderFilter in the next tutorial.

Example Project

Dependencies and Technologies Used :

  • spring-boot-starter-web 1.4.4.RELEASE: Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container.
    Corresponding Spring version: 4.3.6.RELEASE
  • tomcat-embed-jasper 8.5.11: Core Tomcat implementation.
  • JDK 1.8
  • Maven 3.3.9

Spring Mvc Etag Header Example Select All Download
  • etag-header-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • resources
        • webapp
          • WEB-INF
            • pages

See Also