Skip to content

Commit 88e6544

Browse files
committed
Fix regression in WebFlux support for WebDAV methods
This commit ensures that WebFlux's RequestMethodsRequestCondition supports HTTP methods that are not in the RequestMethod enum. - RequestMethod::resolve is introduced, to convert from a HttpMethod (name) to enum values. - RequestMethod::asHttpMethod is introduced, to convert from enum value to HttpMethod. - HttpMethod::valueOf replaced Map-based lookup to a switch statement - Enabled tests that check for WebDAV methods See gh-27697 Closes gh-29981
1 parent 1999c78 commit 88e6544

File tree

6 files changed

+151
-43
lines changed

6 files changed

+151
-43
lines changed

Diff for: spring-web/src/main/java/org/springframework/http/HttpMethod.java

+11-14
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
package org.springframework.http;
1818

1919
import java.io.Serializable;
20-
import java.util.Arrays;
21-
import java.util.Map;
22-
import java.util.function.Function;
23-
import java.util.stream.Collectors;
2420

2521
import org.springframework.lang.Nullable;
2622
import org.springframework.util.Assert;
@@ -88,9 +84,6 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
8884

8985
private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE };
9086

91-
private static final Map<String, HttpMethod> mappings = Arrays.stream(values)
92-
.collect(Collectors.toUnmodifiableMap(HttpMethod::name, Function.identity()));
93-
9487

9588
private final String name;
9689

@@ -121,13 +114,17 @@ public static HttpMethod[] values() {
121114
*/
122115
public static HttpMethod valueOf(String method) {
123116
Assert.notNull(method, "Method must not be null");
124-
HttpMethod result = mappings.get(method);
125-
if (result != null) {
126-
return result;
127-
}
128-
else {
129-
return new HttpMethod(method);
130-
}
117+
return switch (method) {
118+
case "GET" -> GET;
119+
case "HEAD" -> HEAD;
120+
case "POST" -> POST;
121+
case "PUT" -> PUT;
122+
case "PATCH" -> PATCH;
123+
case "DELETE" -> DELETE;
124+
case "OPTIONS" -> OPTIONS;
125+
case "TRACE" -> TRACE;
126+
default -> new HttpMethod(method);
127+
};
131128
}
132129

133130
/**

Diff for: spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java

+61-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.web.bind.annotation;
1818

19+
import org.springframework.http.HttpMethod;
20+
import org.springframework.lang.Nullable;
21+
import org.springframework.util.Assert;
22+
1923
/**
2024
* Enumeration of HTTP request methods. Intended for use with the
2125
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
@@ -34,6 +38,62 @@
3438
*/
3539
public enum RequestMethod {
3640

37-
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
41+
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
42+
43+
44+
/**
45+
* Resolve the given method value to an {@code RequestMethod} enum value.
46+
* Returns {@code null} if {@code method} has no corresponding value.
47+
* @param method the method value as a String
48+
* @return the corresponding {@code RequestMethod}, or {@code null} if not found
49+
* @since 6.0.6
50+
*/
51+
@Nullable
52+
public static RequestMethod resolve(String method) {
53+
Assert.notNull(method, "Method must not be null");
54+
return switch (method) {
55+
case "GET" -> GET;
56+
case "HEAD" -> HEAD;
57+
case "POST" -> POST;
58+
case "PUT" -> PUT;
59+
case "PATCH" -> PATCH;
60+
case "DELETE" -> DELETE;
61+
case "OPTIONS" -> OPTIONS;
62+
case "TRACE" -> TRACE;
63+
default -> null;
64+
};
65+
}
66+
67+
/**
68+
* Resolve the given {@link HttpMethod} to a {@code RequestMethod} enum value.
69+
* Returns {@code null} if {@code httpMethod} has no corresponding value.
70+
* @param httpMethod the http method object
71+
* @return the corresponding {@code RequestMethod}, or {@code null} if not found
72+
* @since 6.0.6
73+
*/
74+
@Nullable
75+
public static RequestMethod resolve(HttpMethod httpMethod) {
76+
Assert.notNull(httpMethod, "HttpMethod must not be null");
77+
return resolve(httpMethod.name());
78+
}
79+
80+
81+
/**
82+
* Return the {@link HttpMethod} corresponding to this {@code RequestMethod}.
83+
* @return the http method for this request method
84+
* @since 6.0.6
85+
*/
86+
public HttpMethod asHttpMethod() {
87+
return switch (this) {
88+
case GET -> HttpMethod.GET;
89+
case HEAD -> HttpMethod.HEAD;
90+
case POST -> HttpMethod.POST;
91+
case PUT -> HttpMethod.PUT;
92+
case PATCH -> HttpMethod.PATCH;
93+
case DELETE -> HttpMethod.DELETE;
94+
case OPTIONS -> HttpMethod.OPTIONS;
95+
case TRACE -> HttpMethod.TRACE;
96+
};
97+
}
3898

3999
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.bind.annotation;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.http.HttpMethod;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* @author Arjen Poutsma
27+
*/
28+
class RequestMethodTests {
29+
30+
@Test
31+
void resolveString() {
32+
String[] methods = new String[]{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"};
33+
for (String httpMethod : methods) {
34+
RequestMethod requestMethod = RequestMethod.resolve(httpMethod);
35+
assertThat(requestMethod).isNotNull();
36+
assertThat(requestMethod.name()).isEqualTo(httpMethod);
37+
}
38+
assertThat(RequestMethod.resolve("PROPFIND")).isNull();
39+
}
40+
41+
@Test
42+
void resolveHttpMethod() {
43+
for (HttpMethod httpMethod : HttpMethod.values()) {
44+
RequestMethod requestMethod = RequestMethod.resolve(httpMethod);
45+
assertThat(requestMethod).isNotNull();
46+
assertThat(requestMethod.name()).isEqualTo(httpMethod.name());
47+
}
48+
assertThat(RequestMethod.resolve(HttpMethod.valueOf("PROPFIND"))).isNull();
49+
}
50+
51+
@Test
52+
void asHttpMethod() {
53+
for (RequestMethod requestMethod : RequestMethod.values()) {
54+
HttpMethod httpMethod = requestMethod.asHttpMethod();
55+
assertThat(httpMethod).isNotNull();
56+
assertThat(httpMethod.name()).isEqualTo(requestMethod.name());
57+
}
58+
}
59+
}

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java

+12-13
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
4646

4747
static {
4848
requestMethodConditionCache = CollectionUtils.newHashMap(RequestMethod.values().length);
49-
for (RequestMethod method : RequestMethod.values()) {
50-
requestMethodConditionCache.put(
51-
HttpMethod.valueOf(method.name()), new RequestMethodsRequestCondition(method));
49+
for (RequestMethod requestMethod : RequestMethod.values()) {
50+
requestMethodConditionCache.put(requestMethod.asHttpMethod(),
51+
new RequestMethodsRequestCondition(requestMethod));
5252
}
5353
}
5454

@@ -150,16 +150,15 @@ private RequestMethodsRequestCondition matchPreFlight(ServerHttpRequest request)
150150
}
151151

152152
@Nullable
153-
private RequestMethodsRequestCondition matchRequestMethod(@Nullable HttpMethod httpMethod) {
154-
if (httpMethod == null) {
155-
return null;
156-
}
157-
RequestMethod requestMethod = RequestMethod.valueOf(httpMethod.name());
158-
if (getMethods().contains(requestMethod)) {
159-
return requestMethodConditionCache.get(httpMethod);
160-
}
161-
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
162-
return requestMethodConditionCache.get(HttpMethod.GET);
153+
private RequestMethodsRequestCondition matchRequestMethod(HttpMethod httpMethod) {
154+
RequestMethod requestMethod = RequestMethod.resolve(httpMethod);
155+
if (requestMethod != null) {
156+
if (getMethods().contains(requestMethod)) {
157+
return requestMethodConditionCache.get(httpMethod);
158+
}
159+
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
160+
return requestMethodConditionCache.get(HttpMethod.GET);
161+
}
163162
}
164163
return null;
165164
}

Diff for: spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java

+6-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.net.URISyntaxException;
2020
import java.util.Collections;
2121

22-
import org.junit.jupiter.api.Disabled;
2322
import org.junit.jupiter.api.Test;
2423

2524
import org.springframework.http.HttpHeaders;
@@ -44,8 +43,6 @@
4443
*/
4544
public class RequestMethodsRequestConditionTests {
4645

47-
// TODO: custom method, CORS pre-flight (see @Disabled)
48-
4946
@Test
5047
public void getMatchingCondition() throws Exception {
5148
testMatch(new RequestMethodsRequestCondition(GET), GET);
@@ -73,19 +70,19 @@ public void getMatchingConditionWithEmptyConditions() throws Exception {
7370
}
7471

7572
@Test
76-
@Disabled
7773
public void getMatchingConditionWithCustomMethod() throws Exception {
7874
ServerWebExchange exchange = getExchange("PROPFIND");
7975
assertThat(new RequestMethodsRequestCondition().getMatchingCondition(exchange)).isNotNull();
8076
assertThat(new RequestMethodsRequestCondition(GET, POST).getMatchingCondition(exchange)).isNull();
8177
}
8278

8379
@Test
84-
@Disabled
85-
public void getMatchingConditionWithCorsPreFlight() throws Exception {
86-
ServerWebExchange exchange = getExchange("OPTIONS");
87-
exchange.getRequest().getHeaders().add("Origin", "https://example.com");
88-
exchange.getRequest().getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT");
80+
public void getMatchingConditionWithCorsPreFlight() {
81+
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.valueOf("OPTIONS"), "/")
82+
.header("Origin", "https://example.com")
83+
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT")
84+
.build();
85+
ServerWebExchange exchange = MockServerWebExchange.from(request);
8986

9087
assertThat(new RequestMethodsRequestCondition().getMatchingCondition(exchange)).isNotNull();
9188
assertThat(new RequestMethodsRequestCondition(PUT).getMatchingCondition(exchange)).isNotNull();

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java

+2-6
Original file line numberDiff line numberDiff line change
@@ -157,19 +157,15 @@ private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request
157157

158158
@Nullable
159159
private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) {
160-
RequestMethod requestMethod;
161-
try {
162-
requestMethod = RequestMethod.valueOf(httpMethodValue);
160+
RequestMethod requestMethod = RequestMethod.resolve(httpMethodValue);
161+
if (requestMethod != null) {
163162
if (getMethods().contains(requestMethod)) {
164163
return requestMethodConditionCache.get(httpMethodValue);
165164
}
166165
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
167166
return requestMethodConditionCache.get(HttpMethod.GET.name());
168167
}
169168
}
170-
catch (IllegalArgumentException ex) {
171-
// Custom request method
172-
}
173169
return null;
174170
}
175171

0 commit comments

Comments
 (0)