Re-initialize API version in each HandlerMapping

Each HandlerMapping may have its own ApiVersionStrategy rules, or may not
have one at all. This change ensures independent decisions.

See gh-36059
This commit is contained in:
rstoyanchev
2026-01-08 09:32:34 +00:00
parent 7fdda1cf0f
commit e0aa116217
4 changed files with 77 additions and 27 deletions
@@ -218,20 +218,17 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
}
private ApiVersionHolder initApiVersion(ServerWebExchange exchange) {
ApiVersionHolder versionHolder = exchange.getAttribute(API_VERSION_ATTRIBUTE);
if (versionHolder == null) {
if (this.apiVersionStrategy == null) {
versionHolder = ApiVersionHolder.EMPTY;
ApiVersionHolder versionHolder;
if (this.apiVersionStrategy == null) {
versionHolder = ApiVersionHolder.EMPTY;
}
else {
try {
Comparable<?> version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
versionHolder = ApiVersionHolder.fromVersion(version);
}
else {
Comparable<?> version;
try {
version = this.apiVersionStrategy.resolveParseAndValidateVersion(exchange);
versionHolder = ApiVersionHolder.fromVersion(version);
}
catch (RuntimeException ex) {
versionHolder = ApiVersionHolder.fromError(ex);
}
catch (RuntimeException ex) {
versionHolder = ApiVersionHolder.fromError(ex);
}
}
exchange.getAttributes().put(API_VERSION_ATTRIBUTE, versionHolder);
@@ -18,6 +18,8 @@ package org.springframework.web.reactive.result.method.annotation;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.RequestEntity;
@@ -29,8 +31,10 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.reactive.accept.StandardApiVersionDeprecationHandler;
import org.springframework.web.reactive.config.ApiVersionConfigurer;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.ResourceHandlerRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer;
import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -73,6 +77,17 @@ public class RequestMappingVersionIntegrationTests extends AbstractRequestMappin
.isEqualTo("<https://example.org/deprecation>; rel=\"deprecation\"; type=\"text/html\"");
}
@Test// gh-36059
void staticResourceWithInvalidApiVersion() throws Exception {
startServer((HttpServer) new TomcatHttpServer());
String url = "http://localhost:" + this.port + "/cp/test/foo.css";
RequestEntity<Void> requestEntity = RequestEntity.get(url).header("API-Version", "Invalid").build();
ResponseEntity<String> entity = getRestTemplate().exchange(requestEntity, String.class);
assertThat(entity.getBody()).isEqualTo("h1 { color:red; }");
}
private ResponseEntity<String> exchangeWithVersion(String version) {
String url = "http://localhost:" + this.port;
RequestEntity<Void> requestEntity = RequestEntity.get(url).header("API-Version", version).build();
@@ -93,6 +108,12 @@ public class RequestMappingVersionIntegrationTests extends AbstractRequestMappin
.addSupportedVersions("1", "1.1", "1.3", "1.6")
.setDeprecationHandler(handler);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/cp/**")
.addResourceLocations("classpath:org/springframework/web/reactive/resource/");
}
}
@@ -591,20 +591,17 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
}
private ApiVersionHolder initApiVersion(HttpServletRequest request) {
ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(API_VERSION_ATTRIBUTE);
if (versionHolder == null) {
if (this.versionStrategy == null) {
versionHolder = ApiVersionHolder.EMPTY;
ApiVersionHolder versionHolder;
if (this.versionStrategy == null) {
versionHolder = ApiVersionHolder.EMPTY;
}
else {
try {
Comparable<?> version = this.versionStrategy.resolveParseAndValidateVersion(request);
versionHolder = ApiVersionHolder.fromVersion(version);
}
else {
Comparable<?> version;
try {
version = this.versionStrategy.resolveParseAndValidateVersion(request);
versionHolder = ApiVersionHolder.fromVersion(version);
}
catch (RuntimeException ex) {
versionHolder = ApiVersionHolder.fromError(ex);
}
catch (RuntimeException ex) {
versionHolder = ApiVersionHolder.fromError(ex);
}
}
request.setAttribute(API_VERSION_ATTRIBUTE, versionHolder);
@@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.UrlResource;
@@ -36,6 +37,7 @@ import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -121,7 +123,7 @@ class ResourceHttpRequestHandlerIntegrationTests {
}
@Test
void testNoResourceFoundException() throws Exception {
void noResourceFoundException() throws Exception {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setServletConfig(this.servletConfig);
context.register(WebConfig.class);
@@ -149,6 +151,29 @@ class ResourceHttpRequestHandlerIntegrationTests {
""");
}
@Test // gh-36059
void invalidApiVersion() throws Exception {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setServletConfig(this.servletConfig);
context.register(WebConfig.class, VersionConfig.class);
context.refresh();
DispatcherServlet servlet = new DispatcherServlet();
servlet.setApplicationContext(context);
servlet.init(this.servletConfig);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/cp/test/foo.css");
MockHttpServletResponse response = new MockHttpServletResponse();
request.addHeader("API-Version", "Invalid");
servlet.service(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo("text/css");
assertThat(response.getContentAsString()).isEqualTo("h1 { color:red; }");
}
private DispatcherServlet initDispatcherServlet(
boolean usePathPatterns, boolean decodingUrlPathHelper, Class<?>... configClasses) throws ServletException {
@@ -257,4 +282,14 @@ class ResourceHttpRequestHandlerIntegrationTests {
private static class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
}
@Configuration
static class VersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-Version");
}
}
}