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 15:47:09 +01:00
parent e1e4d52b61
commit 0b2e3900b9
4 changed files with 58 additions and 45 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 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<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(","));
@@ -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<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);
}
}
}
@@ -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<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;
@@ -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<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(","));
@@ -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<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,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<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}.