← Back to list

ETag Support in Spring Boot for Conditional Requests

ETags are a feature built into HTTP that help with caching and reduce unnecessary data transfers. An ETag is a string that represents the…

Alexander Obregon · 2025-08-21 01:06 · 54 claps · 7.7 min read
#spring-boot #java #http-caching #web-development #programming
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development

ETag Support in Spring Boot for Conditional Requests

Image Source

Image Source

ETags are a feature built into HTTP that help with caching and reduce unnecessary data transfers. An ETag is a string that represents the state of a resource at a certain point in time. Clients can use this value to check if the resource has changed before downloading it again. Spring Boot supports ETags and makes it possible to send them with responses and process the If-None-Match request header to handle conditional requests.

I publish free articles like this daily, if you want to support my work and get access to exclusive content and weekly recaps, consider subscribing to my Substack.

How ETags Work with HTTP

When a browser or API client makes requests to a server, it often asks for the same resource multiple times. Without any caching mechanism, the server would always send back the entire response, even if nothing has changed. ETags give both sides a lightweight way to avoid this waste by marking each version of a resource with an identifier. That identifier changes whenever the resource changes, which makes it possible to ask the server whether the current copy is still valid before transferring data again.

What an ETag Represents

An ETag is essentially a token that identifies a particular version of a resource. The server generates it based on content, a timestamp, or even a version number stored in a database. When the resource is updated, the ETag changes, which lets the client detect differences across requests. ETags come in two flavors: strong and weak. Strong ETags signal that the representation is byte-for-byte identical, while weak ETags allow for semantically equivalent content that may not be exactly the same at the byte level. Weak ETags are prefixed with W/, while strong ones are just quoted strings.

Here’s a simple example of how a server might attach an ETag in raw HTTP:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "e4f1a9d7"
Content-Length: 46

{"id":1,"name":"Document A","status":"active"}

If the content changes, the server produces a new ETag, so the next request will get something different like "9a13f882".

In a Java context, you could generate an ETag from content using a hash function.

import java.security.MessageDigest;
import java.util.HexFormat;
import java.nio.charset.StandardCharsets;

public class EtagGenerator {
    public static String generate(String content) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8));
        return "\"" + HexFormat.of().formatHex(digest) + "\"";
    }
}

Here, an MD5 hash of the content is wrapped in quotes to match the HTTP spec. While MD5 isn’t secure for cryptography, it’s fine for ETag generation because the goal is to detect changes, not protect data.

Another strategy is to generate ETags from database version columns. A table row with an incrementing version field can produce a stable ETag until the resource changes.

String etag = "\"" + document.getVersion() + "\"";

That avoids hashing large blobs of data and ties the ETag directly to how your application tracks resource changes.

The If-None-Match Header

When a client has received an ETag, it can use it in later requests by setting the If-None-Match header. This tells the server, “only send me the full resource if it has changed.” The server checks the header against its current ETag and decides whether to return the full content or a lightweight response.

Here’s how a conditional request looks in raw HTTP:

GET /document/1 HTTP/1.1
Host: api.example.com
If-None-Match: "e4f1a9d7"

If the resource is still the same, the server replies with:

HTTP/1.1 304 Not Modified
ETag: "e4f1a9d7"

No body is sent because the client already has the correct version. This saves bandwidth and processing on both sides.

In Java, handling this behavior can be as direct as comparing strings.

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public ResponseEntity<String> handleRequest(String currentContent, String ifNoneMatchHeader) {
    String etag = "\"" + Integer.toHexString(currentContent.hashCode()) + "\"";
    if (matchesIfNoneMatch(etag, ifNoneMatchHeader)) {
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
    }
    return ResponseEntity.ok().eTag(etag).body(currentContent);
}

private boolean matchesIfNoneMatch(String etag, String header) {
    if (header == null || header.isBlank()) return false;
    for (String raw : header.split(",")) {
        String candidate = raw.trim();
        if ("*".equals(candidate)) return true;       // any current representation
        if (candidate.startsWith("W/")) candidate = candidate.substring(2).trim(); // weak validators match
        if (candidate.equals(etag)) return true;      // quoted strong match
    }
    return false;
}

This method shows the actual logic behind conditional requests: generate an identifier, compare it to what the client sent, and decide on the response.

It’s also worth considering that If-None-Match can contain multiple ETags separated by commas. This allows clients to provide more than one candidate value in case they’re caching multiple variants of the same resource. A server needs to handle these cases by checking against all provided values.

String ifNoneMatch = "\"abc123\", W/\"def456\", \"ghi789\"";
boolean match = false;
for (String raw : ifNoneMatch.split(",")) {
    String c = raw.trim();
    if ("*".equals(c)) { match = true; break; }
    if (c.startsWith("W/")) c = c.substring(2).trim();
    if (c.equals(etag)) { match = true; break; }
}
if (match) {
    // respond with 304
}

That small detail often gets overlooked, but it’s part of the HTTP specification and comes into play when dealing with caching proxies or multiple resource variants.

ETag Support in Spring Boot

Spring Boot is built on top of Spring MVC, and that means a lot of HTTP features are already supported out of the box. ETags are no exception. You can either let the framework take care of generating them automatically through filters or handle the process yourself when you want more control over how the identifiers are produced. Both strategies rely on the same HTTP standards, but they differ in how much responsibility you take in your code.

Using ShallowEtagHeaderFilter

Spring provides a servlet filter named ShallowEtagHeaderFilter that works with very little effort. Its job is to buffer the response, calculate a hash of the response body, and add the ETag header before the content goes back to the client. If the request includes If-None-Match, the filter compares it against the generated value and may shortcut the response to a 304 Not Modified.

To turn it on, you register it as a filter in your configuration.

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 EtagConfig {

    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new ShallowEtagHeaderFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }
}

This works well when the response is not too large, because the filter has to buffer it to compute the hash. That can be a drawback for streaming responses where buffering defeats the purpose, but for typical JSON or HTML responses it is very effective.

Another detail to keep in mind is that this filter uses the response bytes as the basis for the ETag. That means if the response content changes even slightly, a new value will be generated. It makes the filter easy to apply, but sometimes it’s more useful to tie the ETag to application-level data rather than the rendered output. That’s where custom strategies come in.

Generating Custom ETags in Controllers

For applications that want more precise control over caching, generating ETags in the controller or service layer can be a better fit. Instead of hashing the rendered content, you can decide what part of your data determines the freshness of a resource. A version column in a database, a timestamp, or even a UUID that changes on updates can all serve as a stable ETag source.

Here’s a controller that generates its own ETag for a document resource.

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DocumentController {

    @GetMapping("/document")
    public ResponseEntity<String> getDocument() {
        String content = "Sample Document Content";
        String etag = "\"" + Integer.toHexString(content.hashCode()) + "\"";

        HttpHeaders headers = new HttpHeaders();
        headers.setETag(etag);

        return new ResponseEntity<>(content, headers, HttpStatus.OK);
    }
}

The hash is based on the string itself, but you could just as easily use a version field pulled from a database row.

A more data-driven approach could look like this:

String etag = "\"" + record.getVersion() + "\"";
return ResponseEntity.ok().eTag(etag).body(record.getContent());

This way, the ETag only changes when the underlying record changes. That avoids recalculating values for responses that differ only in formatting or representation, giving you tighter control over client caching.

Custom generation is especially useful when responses are expensive to build. Rather than computing and sending the whole resource to every client, the server can trust its own version tracking and only send data if there’s actually something new.

Handling Conditional Requests Manually

Sometimes you’ll want to handle If-None-Match headers explicitly. Spring makes it possible to read request headers directly in controller methods and respond based on the comparison. This approach works well when you have to combine ETag logic with other checks, or when you want to return slightly different responses depending on context.

Here’s an example of manual handling:

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @GetMapping("/product")
    public ResponseEntity<String> getProduct(
            @RequestHeader(value = HttpHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) {

        String content = "Product data";
        String etag = "\"" + Integer.toHexString(content.hashCode()) + "\"";

        if (etag.equals(ifNoneMatch)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
        }
        return ResponseEntity.ok().eTag(etag).body(content);
    }
}

This method checks if the ETag matches and avoids returning the body if the client already has the current version.

There are also cases where clients send multiple ETags in the If-None-Match header. This can happen with caching intermediaries or when clients handle several possible variants of the same resource. To support that, you need to parse the header and check each value.

if (ifNoneMatch != null) {
    for (String candidate : ifNoneMatch.split(",")) {
        if (etag.equals(candidate.trim())) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
        }
    }
}

This makes sure your application stays compliant with the HTTP specification and works correctly even with shared caches or advanced clients.

What Happens in the Background

Spring handles ETags through the servlet API and the HTTP specification. No matter if you rely on the filter or handle things directly in a controller, the process is about comparing string values and deciding whether to return the full content or just headers. With the filter in place, the response is wrapped and buffered so a hash can be calculated. That value becomes the ETag header. When a request arrives with If-None-Match, the filter does the comparison before the body is sent. If there’s a match, the body is dropped and the server replies with a 304 Not Modified status and headers only.

Custom logic in controllers works a little differently. ResponseEntity makes it easy to attach the ETag without extra effort, while the comparison step remains in your code. Spring then handles serialization of the response once you’ve decided what to return. What ties it all together is the fact that browsers, proxies, and HTTP clients already understand how to deal with ETags. After your application generates them, other parts of the network chain such as CDNs or reverse proxies can respect those values automatically.

Conclusion

ETags give Spring Boot applications a way to work with HTTP’s conditional request model in a very direct way. The process comes down to producing an identifier, attaching it to responses, and checking it against incoming headers. Whether the value is generated through a filter or custom logic in a controller, the comparison step is simple, yet it drives an efficient exchange between clients and servers. With that in place, browsers, proxies, and CDNs can all work with your application more effectively, reducing the cost of repeated requests without adding much complexity to your code.

  1. *Spring Framework Documentation on ETag Support*
  2. *Spring Boot Reference Guide*
  3. *MDN HTTP ETag Documentation*
  4. *MDN If-None-Match Header Documentation*

Thanks for reading! If you found this helpful, highlighting, clapping, or leaving a comment really helps me out.

Spring Boot icon by Icons8

Spring Boot icon by Icons8


메타데이터
post_id
d7e6d18f6477
slug
etag-support-in-spring-boot-for-conditional-requests-d7e6d18f6477
url
https://medium.com/@AlexanderObregon/etag-support-in-spring-boot-for-conditional-requests-d7e6d18f6477
canonical_url
https://medium.com/@AlexanderObregon/etag-support-in-spring-boot-for-conditional-requests-d7e6d18f6477
author_url
https://medium.com/@AlexanderObregon
status
ok
fetched_at
2026-08-05 06:22:14