ShallowEtagHeaderFilter automatically generates an ETag value based on the content of the response. That means if we change the content of the target resource, a new ETag value will be generated. ETag generation uses the MD5 algorithm, a hash function that produces a 128-bit hash value.
ShallowEtagHeaderFilter also takes care of populating the response's ETag header and validating the request's If-None-Match header value. That means client code is entirely unaware of any ETag logic.
Example
In this example, we are going to register ShallowEtagHeaderFilter as a servlet filter (by using Spring Boot's FilterRegistrationBean)
Registering ShallowETagHeaderFilter
package com.logicbig.example;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import jakarta.servlet.Filter;
public class WebAppInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
return new Filter[]{
new HeaderLogger(), new ShallowEtagHeaderFilter()};
}
}
Data Store
package com.logicbig.example;
import org.springframework.stereotype.Service;
@Service
public class ResourceStore {
private String data;
public ResourceStore() {
data = "initial Value";
}
public void update(String newData) {
data = newData;
}
public String getData() {
return data;
}
}
The Controller
package com.logicbig.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import java.io.IOException;
@Controller
public class TheController {
@Autowired
private ResourceStore resourceStore;
@ResponseBody
@RequestMapping(value = "/test1")
public String handle1(ServletWebRequest swr) {
return resourceStore.getData();
}
@ResponseBody
@RequestMapping(value = "/test2")
public ResponseEntity<String> handle3(WebRequest swr) {
return ResponseEntity
.ok()
.body(resourceStore.getData());
}
@ResponseBody
@RequestMapping(value = "/update", method = RequestMethod.POST)
public String updateResource(@RequestBody String content) throws IOException {
resourceStore.update(content);
return "data updated";
}
}
Note that our controller is free from all ETag logic.
src/main/webapp/public/myStaticPage.html<html>
<body>
This is a static page.
<br/>
<a href="myStaticPage.html">myStaticPage.html</a>
</body>
</html>
A Servlet Filter to log request and response headers
package com.logicbig.example;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
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 () {
}
}
Running application
mvn spring-boot:run
Output
/test1
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html initial Value
Server Output ----- Request --------- If-None-Match: "" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- ETag: "0260a4b029153e49e1f820ca3d1abfdb1" Content-Length: 13 Date: Mon, 21 Sep 2026 00:21:51 GMT Content-Type: text/plain;charset=iso-8859-1 response status: 200
Please check out the last tutorialto understand to curl options -o, --etag and --etag-compare.
On a subsequent access to /test1 without modifying the target resource:
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html initial Value
Server Output ----- Request --------- If-None-Match: "0260a4b029153e49e1f820ca3d1abfdb1" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- ETag: "0260a4b029153e49e1f820ca3d1abfdb1" Content-Length: 13 Date: Mon, 21 Sep 2026 00:25:42 GMT Content-Type: text/plain;charset=iso-8859-1 response status: 304
Notice the ETag value did not change, but it is still evaluated and validated by ShallowEtagHeaderFilter on every request.
Modify the page content
Now modify the response content by accessing /update POST endpoint:
$ curl -s -X POST "http://localhost:8080/update" -H "Content-Type: text/plain;charset=UTF-8" --data "updated test data" data updated
Now access /test1 again
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html updated test data
Server Output ----- Request --------- If-None-Match: "0260a4b029153e49e1f820ca3d1abfdb1" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- ETag: "00cb70c511ff289a8c25e9ab250570aee" Content-Length: 17 Date: Mon, 21 Sep 2026 00:30:53 GMT Content-Type: text/plain;charset=iso-8859-1 response status: 200
The new content has been sent by the server. The ETag value has also changed this time.
Access /test1 again to confirm server send 304 and same ETag value
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/test1" && type page.html && del page.html && del etag.txt updated test data
Server Output ----- Request --------- If-None-Match: "00cb70c511ff289a8c25e9ab250570aee" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- ETag: "00cb70c511ff289a8c25e9ab250570aee" Content-Length: 17 Date: Mon, 21 Sep 2026 00:30:54 GMT Content-Type: text/plain;charset=iso-8859-1 response status: 304
/test2
The outcome will be similar to what we observed for /test1, using the same actions.
/static/myStaticPage.html
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/static/myStaticPage.html" && type page.html <html> <body> This is a static page. <br/> <a href="myStaticPage.html">myStaticPage.html</a> </body> </html>
Server Output ----- Request --------- If-None-Match: "" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- Accept-Ranges: bytes Cache-Control: max-age=30 ETag: "08f5b2a3112e3bab25277c9506ec69460" Last-Modified: Sat, 04 Feb 2017 03:47:02 GMT Content-Length: 114 Date: Mon, 21 Sep 2026 00:49:08 GMT Content-Type: text/html response status: 200
Accessing the static page again:
 $ curl -s -o page.html --etag-save etag.txt --etag-compare etag.txt "http://localhost:8080/static/myStaticPage.html" && type page.html && del page.html && del etag.txt <html> <body> This is a static page. <br/> <a href="myStaticPage.html">myStaticPage.html</a> </body> </html>
Server Output ----- Request --------- If-None-Match: "08f5b2a3112e3bab25277c9506ec69460" Accept: */* User-Agent: curl/8.21.0 Host: localhost:8080 ----- response --------- Accept-Ranges: bytes Cache-Control: max-age=30 ETag: "08f5b2a3112e3bab25277c9506ec69460" Last-Modified: Sat, 04 Feb 2017 03:47:02 GMT Content-Length: 114 Date: Mon, 21 Sep 2026 00:52:05 GMT Content-Type: text/html response status: 304
The static page request will continue to return 304 until the content of myStaticPage.html changes.
Weak ETag support
ShallowEtagHeaderFilter supports weak ETag values. To enable this, use the following method during ShallowEtagHeaderFilter registration:
public void setWriteWeakETag(boolean writeWeakETag)
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.3.0.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- JDK 25
- Maven 3.9.11
|