Servlet - Working with 'ETag' and 'If-None-Match' headers

[Updated: Feb 3, 2017, Created: Feb 3, 2017]

The response header 'ETag' and the request header 'If-None-Match' are used to cache resources on the clients. Comparing to Last-Modified-Header, using ETag is a more generic and efficient way to cache resources. Also 'Last-Modified' can be understood by the clients, whereas, 'ETag' is entirely understood and used on the server-side logic.

Please check out definitions and general step-by-step usage of ETag/If-None-Match headers if not already familiar.

In this tutorial we will show how to use the two headers in Java Servlet.


Example

The Servlet

@WebServlet(urlPatterns = "/test")
public class MyServlet extends HttpServlet {

  @Override
  protected void doGet (HttpServletRequest req,
                        HttpServletResponse resp)
            throws ServletException, IOException {

      String eTagFromBrowser = req.getHeader("If-None-Match");
      String eTagFromServer = getETag();

      if (eTagFromServer.equals(eTagFromBrowser)) {
          //setting 304 and returning with empty body
          resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
          return;
      }

      resp.addHeader("ETag", getETag());
      resp.setContentType("text/html");
      PrintWriter writer = resp.getWriter();
      writer.write("<h4>My Servlet</h4>");
      writer.write(LocalDateTime.now().toString());
      writer.write("<br/><a href='test'>test</a>");
  }

  private String getETag () {
      //Using hard coded value, in real scenario this value might be auto-generated
      //from the resource content, for example, by using a hash function.
      return "\"version1\"";
  }
}

The logger filter

Like our last example we are going to use same HeaderLogFilter to log the request/response headers.

@WebFilter(urlPatterns = {"/*"})
public class HeaderLogFilter 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 () {
  }
}

Running the web application

Run the embedded Jetty (the plugin is included in the pom.xml):

mvn jetty:run

Output at /test

The console output, showing the ETag header in the response:

----- Request ---------
Cookie: JSESSIONID=424B4AF1976CBE891F2B470AA97C4575
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Host: localhost:8080
Pragma: no-cache
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
ETag: "version1"
Date: Sat, 04 Feb 2017 03:00:09 GMT
Content-Type: text/html;charset=utf-8
response status: 200

Subsequent refresh/reload will not reload the resource instead that will return 304 code with empty body. The browser will use cached copy of the resource:

----- Request ---------
Cookie: JSESSIONID=424B4AF1976CBE891F2B470AA97C4575
If-None-Match: "version1"
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Host: localhost:8080
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
Date: Sat, 04 Feb 2017 03:01:58 GMT
response status: 304

Note that this time (comparing to the last tutorial Last-Modified/If-Modified-Since example) clicking on the link 'test' will also do server side validation. That's the reason we don't need response header like 'Cache-Control:no-cache' to force validation.


Now change the ETag value to 'version2' in the method MyServlet#getETag() and restart the server. On reloading the page or clicking on 'test' link:

----- Request ---------
Cookie: JSESSIONID=424B4AF1976CBE891F2B470AA97C4575
If-None-Match: "version1"
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Referer: http://localhost:8080/test
Host: localhost:8080
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
----- response ---------
ETag: "version2"
Date: Sat, 04 Feb 2017 03:02:42 GMT
Content-Type: text/html;charset=utf-8
response status: 200



Example Project

Dependencies and Technologies Used :

  • javax.servlet-api 3.1.0 Java Servlet API
  • jetty-maven-plugin 9.4.1.v20170120: Jetty maven plugins.
  • JDK 1.8
  • Maven 3.3.9

Servlet Etag Header Example Select All Download
  • servlet-etag-header
    • src
      • main
        • java
          • com
            • logicbig
              • example

See Also