diff --git a/spring-web/src/main/java/org/springframework/http/ETag.java b/spring-web/src/main/java/org/springframework/http/ETag.java
index 0acfaefc04..35c767eccd 100644
--- a/spring-web/src/main/java/org/springframework/http/ETag.java
+++ b/spring-web/src/main/java/org/springframework/http/ETag.java
@@ -47,6 +47,44 @@ public record ETag(String tag, boolean weak) {
return (this == WILDCARD);
}
+ /**
+ * Perform a strong or weak comparison to another {@link ETag}.
+ * @param other the ETag to compare to
+ * @param strong whether to perform strong or weak comparison
+ * @return whether there is a match or not
+ * @since 6.2
+ * @see RFC 9110, Section 8.8.3.2
+ */
+ public boolean compare(ETag other, boolean strong) {
+ if (!StringUtils.hasLength(tag()) || !StringUtils.hasLength(other.tag())) {
+ return false;
+ }
+
+ if (strong && (weak() || other.weak())) {
+ return false;
+ }
+
+ return tag().equals(other.tag());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return (this == other ||
+ (other instanceof ETag oet && this.tag.equals(oet.tag) && this.weak == oet.weak));
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.tag.hashCode();
+ result = 31 * result + Boolean.hashCode(this.weak);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return formattedTag();
+ }
+
/**
* Return the fully formatted tag including "W/" prefix and quotes.
*/
@@ -57,11 +95,23 @@ public record ETag(String tag, boolean weak) {
return (this.weak ? "W/" : "") + "\"" + this.tag + "\"";
}
- @Override
- public String toString() {
- return formattedTag();
- }
+ /**
+ * Create an {@link ETag} instance from a String representation.
+ * @param rawValue the formatted ETag value
+ * @return the created instance
+ * @since 6.2
+ */
+ public static ETag create(String rawValue) {
+ boolean weak = rawValue.startsWith("W/");
+ if (weak) {
+ rawValue = rawValue.substring(2);
+ }
+ if (rawValue.length() > 2 && rawValue.startsWith("\"") && rawValue.endsWith("\"")) {
+ rawValue = rawValue.substring(1, rawValue.length() - 1);
+ }
+ return new ETag(rawValue, weak);
+ }
/**
* Parse entity tags from an "If-Match" or "If-None-Match" header.
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
index 141a258c14..554aca37ce 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
@@ -247,25 +247,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return true;
}
- private boolean matchRequestedETags(Enumeration requestedETags, @Nullable String eTag, boolean weakCompare) {
- if (StringUtils.hasLength(eTag)) {
- eTag = ETag.quoteETagIfNecessary(eTag);
- }
- while (requestedETags.hasMoreElements()) {
- // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
- for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) {
- // only consider "lost updates" checks for unsafe HTTP methods
- if (requestedETag.isWildcard() && StringUtils.hasLength(eTag)
- && !SAFE_METHODS.contains(getRequest().getMethod())) {
- return false;
- }
- if (weakCompare) {
- if (etagWeakMatch(eTag, requestedETag.formattedTag())) {
+ private boolean matchRequestedETags(Enumeration requestedETags, @Nullable String tag, boolean weakCompare) {
+ if (StringUtils.hasLength(tag)) {
+ ETag eTag = ETag.create(tag);
+ boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod());
+ while (requestedETags.hasMoreElements()) {
+ // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
+ for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) {
+ // only consider "lost updates" checks for unsafe HTTP methods
+ if (requestedETag.isWildcard() && isNotSafeMethod) {
return false;
}
- }
- else {
- if (etagStrongMatch(eTag, requestedETag.formattedTag())) {
+ if (requestedETag.compare(eTag, !weakCompare)) {
return false;
}
}
@@ -274,26 +267,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return true;
}
- private boolean etagStrongMatch(@Nullable String first, @Nullable String second) {
- if (!StringUtils.hasLength(first) || first.startsWith("W/")) {
- return false;
- }
- return first.equals(second);
- }
-
- private boolean etagWeakMatch(@Nullable String first, @Nullable String second) {
- if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) {
- return false;
- }
- if (first.startsWith("W/")) {
- first = first.substring(2);
- }
- if (second.startsWith("W/")) {
- second = second.substring(2);
- }
- return first.equals(second);
- }
-
private void updateResponseStateChanging(@Nullable String etag, long lastModifiedTimestamp) {
if (this.notModified && getResponse() != null) {
getResponse().setStatus(HttpStatus.PRECONDITION_FAILED.value());
diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
index 49f64e6223..0500180897 100644
--- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
+++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java
@@ -330,10 +330,11 @@ public class DefaultServerWebExchange implements ServerWebExchange {
if (SAFE_METHODS.contains(getRequest().getMethod())) {
return false;
}
- if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_MATCH))) {
+ List values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_MATCH);
+ if (CollectionUtils.isEmpty(values)) {
return false;
}
- this.notModified = matchRequestedETags(getRequestHeaders().getIfMatch(), eTag, false);
+ this.notModified = matchRequestedETags(values, eTag, false);
}
catch (IllegalArgumentException ex) {
return false;
@@ -341,51 +342,25 @@ public class DefaultServerWebExchange implements ServerWebExchange {
return true;
}
- private boolean matchRequestedETags(List requestedETags, @Nullable String eTag, boolean weakCompare) {
- if (StringUtils.hasLength(eTag)) {
- eTag = ETag.quoteETagIfNecessary(eTag);
- }
- for (String clientEtag : requestedETags) {
- // only consider "lost updates" checks for unsafe HTTP methods
- if ("*".equals(clientEtag) && StringUtils.hasLength(eTag)
- && !SAFE_METHODS.contains(getRequest().getMethod())) {
- return false;
- }
- // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3
- if (weakCompare) {
- if (eTagWeakMatch(eTag, clientEtag)) {
- return false;
- }
- }
- else {
- if (eTagStrongMatch(eTag, clientEtag)) {
- return false;
+ private boolean matchRequestedETags(List requestedETagValues, @Nullable String tag, boolean weakCompare) {
+ if (StringUtils.hasLength(tag)) {
+ ETag eTag = ETag.create(tag);
+ boolean isNotSafeMethod = !SAFE_METHODS.contains(getRequest().getMethod());
+ for (String eTagValue : requestedETagValues) {
+ for (ETag requestedETag : ETag.parse(eTagValue)) {
+ // only consider "lost updates" checks for unsafe HTTP methods
+ if (requestedETag.isWildcard() && isNotSafeMethod) {
+ return false;
+ }
+ if (requestedETag.compare(eTag, !weakCompare)) {
+ return false;
+ }
}
}
}
return true;
}
- private boolean eTagStrongMatch(@Nullable String first, @Nullable String second) {
- if (!StringUtils.hasLength(first) || first.startsWith("W/")) {
- return false;
- }
- return first.equals(second);
- }
-
- private boolean eTagWeakMatch(@Nullable String first, @Nullable String second) {
- if (!StringUtils.hasLength(first) || !StringUtils.hasLength(second)) {
- return false;
- }
- if (first.startsWith("W/")) {
- first = first.substring(2);
- }
- if (second.startsWith("W/")) {
- second = second.substring(2);
- }
- return first.equals(second);
- }
-
private void updateResponseStateChanging(@Nullable String eTag, Instant lastModified) {
if (this.notModified) {
getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
@@ -400,7 +375,8 @@ public class DefaultServerWebExchange implements ServerWebExchange {
if (CollectionUtils.isEmpty(getRequestHeaders().get(HttpHeaders.IF_NONE_MATCH))) {
return false;
}
- this.notModified = !matchRequestedETags(getRequestHeaders().getIfNoneMatch(), eTag, true);
+ List values = getRequestHeaders().getOrEmpty(HttpHeaders.IF_NONE_MATCH);
+ this.notModified = !matchRequestedETags(values, eTag, true);
}
catch (IllegalArgumentException ex) {
return false;