Skip to content

Commit 6d6f657

Browse files
committed
Add option to not use detected versions
Closes gh-34775
1 parent 0c6a26a commit 6d6f657

File tree

5 files changed

+118
-39
lines changed

5 files changed

+118
-39
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
4848

4949
private final Set<Comparable<?>> supportedVersions = new TreeSet<>();
5050

51+
private final boolean detectSupportedVersions;
52+
53+
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
54+
5155

5256
/**
5357
* Create an instance.
@@ -59,10 +63,13 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
5963
* validation fails with {@link MissingApiVersionException}
6064
* @param defaultVersion a default version to assign to requests that
6165
* don't specify one
66+
* @param detectSupportedVersions whether to use API versions that appear in
67+
* mappings for supported version validation (true), or use only explicitly
68+
* configured versions (false).
6269
*/
6370
public DefaultApiVersionStrategy(
6471
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
65-
boolean versionRequired, @Nullable String defaultVersion) {
72+
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions) {
6673

6774
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
6875
Assert.notNull(versionParser, "ApiVersionParser is required");
@@ -71,6 +78,7 @@ public DefaultApiVersionStrategy(
7178
this.versionParser = versionParser;
7279
this.versionRequired = (versionRequired && defaultVersion == null);
7380
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
81+
this.detectSupportedVersions = detectSupportedVersions;
7482
}
7583

7684

@@ -80,18 +88,37 @@ public DefaultApiVersionStrategy(
8088
}
8189

8290
/**
83-
* Add to the list of known, supported versions to check against in
84-
* {@link ApiVersionStrategy#validateVersion}. Request versions that are not
85-
* in the supported result in {@link InvalidApiVersionException}
86-
* in {@link ApiVersionStrategy#validateVersion}.
87-
* @param versions the versions to add
91+
* Add to the list of supported versions to check against in
92+
* {@link ApiVersionStrategy#validateVersion} before raising
93+
* {@link InvalidApiVersionException} for unknown versions.
94+
* <p>By default, actual version values that appear in request mappings are
95+
* considered supported, and use of this method is optional. However, if you
96+
* prefer to use only explicitly configured, supported versions, then set
97+
* {@code detectSupportedVersions} flag to {@code false}.
98+
* @param versions the supported versions to add
99+
* @see #addMappedVersion(String...)
88100
*/
89101
public void addSupportedVersion(String... versions) {
90102
for (String version : versions) {
91103
this.supportedVersions.add(parseVersion(version));
92104
}
93105
}
94106

107+
/**
108+
* Internal method to add to the list of actual version values that appear in
109+
* request mappings, which allows supported versions to be discovered rather
110+
* than {@link #addSupportedVersion(String...) configured}.
111+
* <p>If you prefer to use explicitly configured, supported versions only,
112+
* set the {@code detectSupportedVersions} flag to {@code false}.
113+
* @param versions the versions to add
114+
* @see #addSupportedVersion(String...)
115+
*/
116+
public void addMappedVersion(String... versions) {
117+
for (String version : versions) {
118+
this.detectedVersions.add(parseVersion(version));
119+
}
120+
}
121+
95122
@Override
96123
public @Nullable String resolveVersion(ServerWebExchange exchange) {
97124
for (ApiVersionResolver resolver : this.versionResolvers) {
@@ -118,15 +145,24 @@ public void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExc
118145
return;
119146
}
120147

121-
if (!this.supportedVersions.contains(requestVersion)) {
148+
if (!isSupportedVersion(requestVersion)) {
122149
throw new InvalidApiVersionException(requestVersion.toString());
123150
}
124151
}
125152

153+
private boolean isSupportedVersion(Comparable<?> requestVersion) {
154+
return (this.supportedVersions.contains(requestVersion) ||
155+
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
156+
}
157+
126158
@Override
127159
public String toString() {
128-
return "DefaultApiVersionStrategy[supportedVersions=" + this.supportedVersions +
129-
", versionRequired=" + this.versionRequired + ", defaultVersion=" + this.defaultVersion + "]";
160+
return "DefaultApiVersionStrategy[" +
161+
"supportedVersions=" + this.supportedVersions + ", " +
162+
"mappedVersions=" + this.detectedVersions + ", " +
163+
"detectSupportedVersions=" + this.detectSupportedVersions + ", " +
164+
"versionRequired=" + this.versionRequired + ", " +
165+
"defaultVersion=" + this.defaultVersion + "]";
130166
}
131167

132168
}

spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jspecify.annotations.Nullable;
2727

2828
import org.springframework.web.accept.ApiVersionParser;
29+
import org.springframework.web.accept.InvalidApiVersionException;
2930
import org.springframework.web.accept.SemanticApiVersionParser;
3031
import org.springframework.web.reactive.accept.ApiVersionResolver;
3132
import org.springframework.web.reactive.accept.ApiVersionStrategy;
@@ -50,6 +51,8 @@ public class ApiVersionConfigurer {
5051

5152
private final Set<String> supportedVersions = new LinkedHashSet<>();
5253

54+
private boolean detectSupportedVersions = true;
55+
5356

5457
/**
5558
* Add a resolver that extracts the API version from a request header.
@@ -125,28 +128,43 @@ public ApiVersionConfigurer setDefaultVersion(@Nullable String defaultVersion) {
125128
}
126129

127130
/**
128-
* Add to the list of supported versions to validate request versions against.
129-
* Request versions that are not supported result in
130-
* {@link org.springframework.web.accept.InvalidApiVersionException}.
131-
* <p>Note that the set of supported versions is populated from versions
132-
* listed in controller mappings. Therefore, typically you do not have to
133-
* manage this list except for the initial API version, when controller
134-
* don't have to have a version to start.
135-
* @param versions supported versions to add
131+
* Add to the list of supported versions to check against before raising
132+
* {@link InvalidApiVersionException} for unknown versions.
133+
* <p>By default, actual version values that appear in request mappings are
134+
* used for validation. Therefore, use of this method is optional. However,
135+
* if you prefer to use explicitly configured, supported versions only, then
136+
* set {@link #detectSupportedVersions} to {@code false}.
137+
* <p>Note that the initial API version, if not explicitly declared in any
138+
* request mappings, may need to be declared here instead as a supported
139+
* version.
140+
* @param versions supported version values to add
136141
*/
137142
public ApiVersionConfigurer addSupportedVersions(String... versions) {
138143
Collections.addAll(this.supportedVersions, versions);
139144
return this;
140145
}
141146

147+
/**
148+
* Whether to use versions from mappings for supported version validation.
149+
* <p>By default, this is {@code true} in which case mapped versions are
150+
* considered supported versions. Set this to {@code false} if you want to
151+
* use only explicitly configured {@link #addSupportedVersions(String...)
152+
* supported versions}.
153+
* @param detect whether to use detected versions for validation
154+
*/
155+
public ApiVersionConfigurer detectSupportedVersions(boolean detect) {
156+
this.detectSupportedVersions = detect;
157+
return this;
158+
}
159+
142160
protected @Nullable ApiVersionStrategy getApiVersionStrategy() {
143161
if (this.versionResolvers.isEmpty()) {
144162
return null;
145163
}
146164

147165
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
148166
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
149-
this.versionRequired, this.defaultVersion);
167+
this.versionRequired, this.defaultVersion, this.detectSupportedVersions);
150168

151169
this.supportedVersions.forEach(strategy::addSupportedVersion);
152170

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ protected boolean isHandler(Class<?> beanType) {
239239
if (requestMappingInfo != null && this.apiVersionStrategy instanceof DefaultApiVersionStrategy davs) {
240240
String version = requestMappingInfo.getVersionCondition().getVersion();
241241
if (version != null) {
242-
davs.addSupportedVersion(version);
242+
davs.addMappedVersion(version);
243243
}
244244
}
245245

spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,39 +36,64 @@
3636
*/
3737
public class DefaultApiVersionStrategiesTests {
3838

39-
private final SemanticApiVersionParser parser = new SemanticApiVersionParser();
39+
private static final SemanticApiVersionParser parser = new SemanticApiVersionParser();
40+
41+
private static final ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
4042

4143

4244
@Test
43-
void defaultVersion() {
44-
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3");
45-
ApiVersionStrategy strategy = initVersionStrategy(version.toString());
45+
void defaultVersionIsParsed() {
46+
String version = "1.2.3";
47+
ApiVersionStrategy strategy = apiVersionStrategy(version, false);
48+
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
49+
}
4650

47-
assertThat(strategy.getDefaultVersion()).isEqualTo(version);
51+
@Test
52+
void validateSupportedVersion() {
53+
String version = "1.2";
54+
DefaultApiVersionStrategy strategy = apiVersionStrategy();
55+
strategy.addSupportedVersion(version);
56+
validateVersion(version, strategy);
4857
}
4958

5059
@Test
51-
void supportedVersions() {
52-
SemanticApiVersionParser.Version v1 = this.parser.parseVersion("1");
53-
SemanticApiVersionParser.Version v2 = this.parser.parseVersion("2");
54-
SemanticApiVersionParser.Version v9 = this.parser.parseVersion("9");
60+
void validateUnsupportedVersion() {
61+
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
62+
.isInstanceOf(InvalidApiVersionException.class);
63+
}
5564

56-
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
57-
strategy.addSupportedVersion(v1.toString());
58-
strategy.addSupportedVersion(v2.toString());
65+
@Test
66+
void validateDetectedSupportedVersion() {
67+
String version = "1.2";
68+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
69+
strategy.addMappedVersion(version);
70+
validateVersion(version, strategy);
71+
}
5972

60-
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
61-
strategy.validateVersion(v1, exchange);
62-
strategy.validateVersion(v2, exchange);
73+
@Test
74+
void validateWhenDetectSupportedVersionsIsOff() {
75+
String version = "1.2";
76+
DefaultApiVersionStrategy strategy = apiVersionStrategy();
77+
strategy.addMappedVersion(version);
6378

64-
assertThatThrownBy(() -> strategy.validateVersion(v9, exchange))
79+
assertThatThrownBy(() -> strategy.validateVersion(version, exchange))
6580
.isInstanceOf(InvalidApiVersionException.class);
6681
}
6782

68-
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
69-
return new DefaultApiVersionStrategy(
83+
private static DefaultApiVersionStrategy apiVersionStrategy() {
84+
return apiVersionStrategy(null, false);
85+
}
86+
87+
private static DefaultApiVersionStrategy apiVersionStrategy(
88+
@Nullable String defaultValue, boolean detectSupportedVersions) {
89+
90+
return new DefaultApiVersionStrategy(
7091
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
71-
new SemanticApiVersionParser(), true, defaultValue);
92+
parser, true, defaultValue, detectSupportedVersions);
93+
}
94+
95+
private static void validateVersion(String version, DefaultApiVersionStrategy strategy) {
96+
strategy.validateVersion(parser.parseVersion(version), exchange);
7297
}
7398

7499
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ void setUp() {
5050
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
5151
return new DefaultApiVersionStrategy(
5252
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
53-
new SemanticApiVersionParser(), true, defaultValue);
53+
new SemanticApiVersionParser(), true, defaultValue, false);
5454
}
5555

5656
@Test

0 commit comments

Comments
 (0)