diff --git a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc index 917fcc95bdf..2065a962f1e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc @@ -49,6 +49,11 @@ This strategy resolves the API version from a request. The WebFlux config provid options to resolve from a header, query parameter, media type parameter, or from the URL path. You can also use a custom `ApiVersionResolver`. +NOTE: The path resolver always resolves the version from the specified path segment, or +raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other +resolvers. + + diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc index f9f0afd77cc..4cb7dd94797 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc @@ -49,6 +49,11 @@ This strategy resolves the API version from a request. The MVC config provides b options to resolve from a header, query parameter, media type parameter, or from the URL path. You can also use a custom `ApiVersionResolver`. +NOTE: The path resolver always resolves the version from the specified path segment, or +raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other +resolvers. + + diff --git a/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java index 4be157e33af..9d5a93d26ee 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java +++ b/spring-web/src/main/java/org/springframework/web/accept/PathApiVersionResolver.java @@ -17,7 +17,6 @@ package org.springframework.web.accept; import jakarta.servlet.http.HttpServletRequest; -import org.jspecify.annotations.Nullable; import org.springframework.http.server.PathContainer; import org.springframework.http.server.RequestPath; @@ -27,6 +26,11 @@ import org.springframework.web.util.ServletRequestPathUtils; /** * {@link ApiVersionResolver} that extract the version from a path segment. * + *
Note that this resolver will either resolve the version from the specified + * path segment, or raise an {@link InvalidApiVersionException}, e.g. if there + * are not enough path segments. It never returns {@code null}, and therefore + * cannot yield to other resolvers. + * * @author Rossen Stoyanchev * @since 7.0 */ @@ -47,17 +51,18 @@ public class PathApiVersionResolver implements ApiVersionResolver { @Override - public @Nullable String resolveVersion(HttpServletRequest request) { - if (ServletRequestPathUtils.hasParsedRequestPath(request)) { - RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request); - int i = 0; - for (PathContainer.Element e : path.pathWithinApplication().elements()) { - if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) { - return e.value(); - } + public String resolveVersion(HttpServletRequest request) { + if (!ServletRequestPathUtils.hasParsedRequestPath(request)) { + throw new IllegalStateException("Expected parsed request path"); + } + RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request); + int i = 0; + for (PathContainer.Element element : path.pathWithinApplication().elements()) { + if (element instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) { + return element.value(); } } - return null; + throw new InvalidApiVersionException("No path segment at index " + this.pathSegmentIndex); } } diff --git a/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java b/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java index e4fadc59853..2b5c56b78d3 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/PathApiVersionResolverTests.java @@ -22,6 +22,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.util.ServletRequestPathUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link PathApiVersionResolver}. @@ -35,6 +36,11 @@ public class PathApiVersionResolverTests { testResolve(1, "/app/1.1/path", "1.1"); } + @Test + void insufficientPathSegments() { + assertThatThrownBy(() -> testResolve(0, "/", "1.0")).isInstanceOf(InvalidApiVersionException.class); + } + private static void testResolve(int index, String requestUri, String expected) { MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); try { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java index 777c5947b72..2da6819498b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/accept/PathApiVersionResolver.java @@ -16,15 +16,19 @@ package org.springframework.web.reactive.accept; -import org.jspecify.annotations.Nullable; - import org.springframework.http.server.PathContainer; import org.springframework.util.Assert; +import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.server.ServerWebExchange; /** * {@link ApiVersionResolver} that extract the version from a path segment. * + *
Note that this resolver will either resolve the version from the specified + * path segment, or raise an {@link InvalidApiVersionException}, e.g. if there + * are not enough path segments. It never returns {@code null}, and therefore + * cannot yield to other resolvers. + * * @author Rossen Stoyanchev * @since 7.0 */ @@ -45,14 +49,14 @@ public class PathApiVersionResolver implements ApiVersionResolver { @Override - public @Nullable String resolveVersion(ServerWebExchange exchange) { + public String resolveVersion(ServerWebExchange exchange) { int i = 0; for (PathContainer.Element e : exchange.getRequest().getPath().pathWithinApplication().elements()) { if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) { return e.value(); } } - return null; + throw new InvalidApiVersionException("No path segment at index " + this.pathSegmentIndex); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java index 049069acfcd..0392f4dd878 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java @@ -79,16 +79,6 @@ public class ApiVersionConfigurer { return this; } - /** - * Add a resolver that extracts the API version from a path segment. - * @param index the index of the path segment to check; e.g. for URL's like - * "/{version}/..." use index 0, for "/api/{version}/..." index 1. - */ - public ApiVersionConfigurer usePathSegment(int index) { - this.versionResolvers.add(new PathApiVersionResolver(index)); - return this; - } - /** * Add resolver to extract the version from a media type parameter found in * the Accept or Content-Type headers. @@ -101,6 +91,18 @@ public class ApiVersionConfigurer { return this; } + /** + * Add a resolver that extracts the API version from a path segment. + *
Note that this resolver never returns {@code null}, and therefore + * cannot yield to other resolvers, see {@link org.springframework.web.accept.PathApiVersionResolver}. + * @param index the index of the path segment to check; e.g. for URL's like + * "/{version}/..." use index 0, for "/api/{version}/..." index 1. + */ + public ApiVersionConfigurer usePathSegment(int index) { + this.versionResolvers.add(new PathApiVersionResolver(index)); + return this; + } + /** * Add custom resolvers to resolve the API version. * @param resolvers the resolvers to use diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java index 3feb8266f3c..3e3ec3076fa 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/accept/PathApiVersionResolverTests.java @@ -18,11 +18,13 @@ package org.springframework.web.reactive.accept; import org.junit.jupiter.api.Test; +import org.springframework.web.accept.InvalidApiVersionException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link org.springframework.web.accept.PathApiVersionResolver}. @@ -36,6 +38,11 @@ public class PathApiVersionResolverTests { testResolve(1, "/app/1.1/path", "1.1"); } + @Test + void insufficientPathSegments() { + assertThatThrownBy(() -> testResolve(0, "/", "1.0")).isInstanceOf(InvalidApiVersionException.class); + } + private static void testResolve(int index, String requestUri, String expected) { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(requestUri)); String actual = new PathApiVersionResolver(index).resolveVersion(exchange); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java index 5042170e6a7..e3e3b1c60a1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java @@ -80,16 +80,6 @@ public class ApiVersionConfigurer { return this; } - /** - * Add resolver to extract the version from a path segment. - * @param index the index of the path segment to check; e.g. for URL's like - * "/{version}/..." use index 0, for "/api/{version}/..." index 1. - */ - public ApiVersionConfigurer usePathSegment(int index) { - this.versionResolvers.add(new PathApiVersionResolver(index)); - return this; - } - /** * Add resolver to extract the version from a media type parameter found in * the Accept or Content-Type headers. @@ -102,6 +92,18 @@ public class ApiVersionConfigurer { return this; } + /** + * Add resolver to extract the version from a path segment. + *
Note that this resolver never returns {@code null}, and therefore + * cannot yield to other resolvers, see {@link PathApiVersionResolver}. + * @param index the index of the path segment to check; e.g. for URL's like + * "/{version}/..." use index 0, for "/api/{version}/..." index 1. + */ + public ApiVersionConfigurer usePathSegment(int index) { + this.versionResolvers.add(new PathApiVersionResolver(index)); + return this; + } + /** * Add custom resolvers to resolve the API version. * @param resolvers the resolvers to use