java 102 lines · 3 tabs

Conditional GET with ETag ShallowEtagHeaderFilter and Cache-Control in Spring Boot

Shared by codesnips Sep 2026
3 tabs
package com.example.caching.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;

@Configuration
public class ShallowEtagConfig {

    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter> registration =
                new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
        registration.addUrlPatterns("/api/*");
        registration.setName("etagFilter");
        registration.setOrder(1);
        return registration;
    }
}
3 files · java Explain with highlit

This snippet shows how a Spring Boot service returns conditional GET responses so clients can skip re-downloading unchanged resources. The core idea is HTTP validation caching: the server tags each response body with an ETag, the client echoes it back in If-None-Match, and when nothing changed the server answers 304 Not Modified with an empty body. This trades a little server CPU (hashing the payload) for large savings in bandwidth and client render time.

In ShallowEtagConfig, a ShallowEtagHeaderFilter is registered as a servlet Filter. It is called shallow because it buffers the fully rendered response, computes an MD5 hash over the bytes, and sets that as the ETag. On the next request it compares the incoming If-None-Match against the freshly computed tag; on a match it discards the body and writes a 304. The registration is scoped to /api/* via addUrlPatterns so only API responses pay the hashing cost, and setBeanName keeps it distinct from any auto-registered instance. Note the trade-off: the body is still generated in full, so this saves network transfer, not server work.

ProductController layers expiration caching on top of validation caching. Its getProduct handler builds a CacheControl with maxAge plus mustRevalidate, so browsers may reuse the response for a short window and then revalidate using the ETag. Because ShallowEtagHeaderFilter handles ETag generation and the 304 short-circuit automatically, the controller never touches If-None-Match itself — it simply returns the resource and its cache policy through ResponseEntity.

ProductServiceTest uses MockMvc to prove the behavior end to end: a first GET returns 200 with an ETag header, and a second GET carrying that value in If-None-Match returns 304 with no body. A key pitfall the test guards against is filter ordering — the ETag filter must wrap the controller, and maxAge must not be so long that clients never revalidate. This pattern fits read-heavy, cacheable endpoints where payloads change infrequently and correctness of staleness matters.


Related snips

Share this code

Here's the card — post it anywhere.

Conditional GET with ETag ShallowEtagHeaderFilter and Cache-Control in Spring Boot — share card
Link copied