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
This commit is contained in:
Brian Clozel
2026-03-20 16:13:47 +01:00
parent 506c6f1777
commit e607f1c30f
4 changed files with 58 additions and 47 deletions
@@ -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 reactor.core.publisher.Mono;
@@ -134,15 +132,12 @@ public class CachingResourceResolver extends AbstractResourceResolver {
@Nullable
private String getContentCodingKey(ServerWebExchange exchange) {
String header = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
if (!StringUtils.hasText(header)) {
List<String> 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(","));
@@ -36,9 +36,9 @@ 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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
@@ -141,23 +141,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return resource;
}
String acceptEncoding = getAcceptEncoding(exchange);
if (acceptEncoding == null) {
List<String> 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);
}
}
}
@@ -166,13 +166,6 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
});
}
@Nullable
private 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 +181,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return chain.resolveUrlPath(resourceUrlPath, locations);
}
/**
* Parse the accepted encodings from the given HTTP exchange request.
*/
static List<String> 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}.
@@ -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;
@@ -28,7 +26,6 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -141,15 +138,12 @@ public class CachingResourceResolver extends AbstractResourceResolver {
@Nullable
private String getContentCodingKey(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
if (!StringUtils.hasText(header)) {
List<String> 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(","));
@@ -38,6 +38,7 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
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<String> 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,12 +165,6 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return resource;
}
@Nullable
private 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) {
@@ -186,6 +181,23 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return chain.resolveUrlPath(resourceUrlPath, locations);
}
/**
* Parse the accepted encodings from the given HTTP request.
*/
static List<String> 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}.