From 0b2e3900b9ce57873f3393ba9e026cb58d8721ba Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 20 Mar 2026 15:47:09 +0100 Subject: [PATCH] Share codings resolution in resource resolvers Prior to this commit, different resource resolvers would resolve accepted codings from the HTTP request sent by the client. This would be done with different implementations, which could lead to resolution errors and desynchronizations. This commit now introduced a new shared method in `EncodedResourceResolver` (Servlet and Reactive) to perform a consisten resolution. Fixes gh-36507 --- .../resource/CachingResourceResolver.java | 13 ++----- .../resource/EncodedResourceResolver.java | 39 ++++++++++++------- .../resource/CachingResourceResolver.java | 14 ++----- .../resource/EncodedResourceResolver.java | 37 ++++++++++++------ 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java index 9c05bbde8ad..b7d205bc55c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CachingResourceResolver.java @@ -17,10 +17,8 @@ package org.springframework.web.reactive.resource; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -133,15 +131,12 @@ public class CachingResourceResolver extends AbstractResourceResolver { } private @Nullable String getContentCodingKey(ServerWebExchange exchange) { - String header = exchange.getRequest().getHeaders().getFirst("Accept-Encoding"); - if (!StringUtils.hasText(header)) { + List acceptedCodings = EncodedResourceResolver.parseAcceptEncoding(exchange); + if (acceptedCodings.isEmpty()) { return null; } - return Arrays.stream(StringUtils.tokenizeToStringArray(header, ",")) - .map(token -> { - int index = token.indexOf(';'); - return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase(Locale.ROOT); - }) + return acceptedCodings + .stream() .filter(this.contentCodings::contains) .sorted() .collect(Collectors.joining(",")); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java index 07c9345695b..000af7a8e8e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java @@ -38,8 +38,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; /** @@ -142,23 +142,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return resource; } - String acceptEncoding = getAcceptEncoding(exchange); - if (acceptEncoding == null) { + List acceptedCodings = parseAcceptEncoding(exchange); + if (acceptedCodings.isEmpty()) { return resource; } - for (String coding : this.contentCodings) { - if (acceptEncoding.contains(coding)) { + for (String acceptedCoding : acceptedCodings) { + if (this.contentCodings.contains(acceptedCoding)) { try { - String extension = getExtension(coding); - Resource encoded = new EncodedResource(resource, coding, extension); + String extension = getExtension(acceptedCoding); + Resource encoded = new EncodedResource(resource, acceptedCoding, extension); if (encoded.exists()) { return encoded; } } catch (IOException ex) { logger.trace(exchange.getLogPrefix() + - "No " + coding + " resource for [" + resource.getFilename() + "]", ex); + "No " + acceptedCoding + " resource for [" + resource.getFilename() + "]", ex); } } } @@ -167,12 +167,6 @@ public class EncodedResourceResolver extends AbstractResourceResolver { }); } - private @Nullable String getAcceptEncoding(ServerWebExchange exchange) { - ServerHttpRequest request = exchange.getRequest(); - String header = request.getHeaders().getFirst(HttpHeaders.ACCEPT_ENCODING); - return (header != null ? header.toLowerCase(Locale.ROOT) : null); - } - private String getExtension(String coding) { String extension = this.extensions.get(coding); if (extension == null) { @@ -188,6 +182,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return chain.resolveUrlPath(resourceUrlPath, locations); } + /** + * Parse the accepted encodings from the given HTTP exchange request. + */ + static List parseAcceptEncoding(ServerWebExchange exchange) { + String header = exchange.getRequest().getHeaders().getFirst("Accept-Encoding"); + if (!StringUtils.hasText(header)) { + return Collections.emptyList(); + } + header = header.toLowerCase(Locale.ROOT); + return Arrays.stream(StringUtils.tokenizeToStringArray(header, ",")) + .map(token -> { + int index = token.indexOf(';'); + return (index >= 0 ? token.substring(0, index) : token).trim(); + }) + .toList(); + } + /** * An encoded {@link HttpResource}. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java index 17fd1df9465..f8575376d76 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java @@ -17,10 +17,8 @@ package org.springframework.web.servlet.resource; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; @@ -29,7 +27,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -139,15 +136,12 @@ public class CachingResourceResolver extends AbstractResourceResolver { } private @Nullable String getContentCodingKey(HttpServletRequest request) { - String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING); - if (!StringUtils.hasText(header)) { + List acceptedCodings = EncodedResourceResolver.parseAcceptEncoding(request); + if (acceptedCodings.isEmpty()) { return null; } - return Arrays.stream(StringUtils.tokenizeToStringArray(header, ",")) - .map(token -> { - int index = token.indexOf(';'); - return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase(Locale.ROOT); - }) + return acceptedCodings + .stream() .filter(this.contentCodings::contains) .sorted() .collect(Collectors.joining(",")); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java index 0b80b3b40d8..b42abc226c1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java @@ -39,6 +39,7 @@ import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Resolver that delegates to the chain, and if a resource is found, it then @@ -139,23 +140,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return resource; } - String acceptEncoding = getAcceptEncoding(request); - if (acceptEncoding == null) { + List acceptedCodings = parseAcceptEncoding(request); + if (acceptedCodings.isEmpty()) { return resource; } - for (String coding : this.contentCodings) { - if (acceptEncoding.contains(coding)) { + for (String acceptedCoding : acceptedCodings) { + if (this.contentCodings.contains(acceptedCoding)) { try { - String extension = getExtension(coding); - Resource encoded = new EncodedResource(resource, coding, extension); + String extension = getExtension(acceptedCoding); + Resource encoded = new EncodedResource(resource, acceptedCoding, extension); if (encoded.exists()) { return encoded; } } catch (IOException ex) { if (logger.isTraceEnabled()) { - logger.trace("No " + coding + " resource for [" + resource.getFilename() + "]", ex); + logger.trace("No " + acceptedCoding + " resource for [" + resource.getFilename() + "]", ex); } } } @@ -164,11 +165,6 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return resource; } - private @Nullable String getAcceptEncoding(HttpServletRequest request) { - String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING); - return (header != null ? header.toLowerCase(Locale.ROOT) : null); - } - private String getExtension(String coding) { String extension = this.extensions.get(coding); if (extension == null) { @@ -184,6 +180,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver { return chain.resolveUrlPath(resourceUrlPath, locations); } + /** + * Parse the accepted encodings from the given HTTP request. + */ + static List parseAcceptEncoding(HttpServletRequest request) { + String header = request.getHeader("Accept-Encoding"); + if (!StringUtils.hasText(header)) { + return Collections.emptyList(); + } + header = header.toLowerCase(Locale.ROOT); + return Arrays.stream(StringUtils.tokenizeToStringArray(header, ",")) + .map(token -> { + int index = token.indexOf(';'); + return (index >= 0 ? token.substring(0, index) : token).trim(); + }) + .toList(); + } + /** * An encoded {@link HttpResource}.