Skip to content

Commit f32aded

Browse files
committed
Consolidate CORS/OPTIONS request mapping
The CORS pre-flight request matching logic for all request conditions was added (in 4.2) to RequestMappingInfo. However the logic for default handling of all HTTP OPTIONS requests for 4.3 unintentionally overrode some of the pre-flight request handling thus causing issues. This commit moves CORS pre-flight matching logic into each respective RequestMethodCondition implementations so each has to consider in one place what happens for pre-flight and for all other requests. Issue: SPR-13130
1 parent 2657514 commit f32aded

File tree

8 files changed

+93
-53
lines changed

8 files changed

+93
-53
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.util.StringUtils;
3131
import org.springframework.web.HttpMediaTypeNotSupportedException;
3232
import org.springframework.web.bind.annotation.RequestMapping;
33+
import org.springframework.web.cors.CorsUtils;
3334
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
3435

3536
/**
@@ -46,6 +47,9 @@
4647
*/
4748
public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> {
4849

50+
private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition();
51+
52+
4953
private final List<ConsumeMediaTypeExpression> expressions;
5054

5155

@@ -160,6 +164,9 @@ public ConsumesRequestCondition combine(ConsumesRequestCondition other) {
160164
*/
161165
@Override
162166
public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
167+
if (CorsUtils.isPreFlightRequest(request)) {
168+
return PRE_FLIGHT_MATCH;
169+
}
163170
if (isEmpty()) {
164171
return this;
165172
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import javax.servlet.http.HttpServletRequest;
2424

2525
import org.springframework.web.bind.annotation.RequestMapping;
26+
import org.springframework.web.cors.CorsUtils;
2627

2728
/**
2829
* A logical conjunction (' && ') request condition that matches a request against
@@ -38,6 +39,9 @@
3839
*/
3940
public final class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> {
4041

42+
private final static HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition();
43+
44+
4145
private final Set<HeaderExpression> expressions;
4246

4347

@@ -105,6 +109,9 @@ public HeadersRequestCondition combine(HeadersRequestCondition other) {
105109
*/
106110
@Override
107111
public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) {
112+
if (CorsUtils.isPreFlightRequest(request)) {
113+
return PRE_FLIGHT_MATCH;
114+
}
108115
for (HeaderExpression expression : expressions) {
109116
if (!expression.match(request)) {
110117
return null;

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.web.accept.ContentNegotiationManager;
3131
import org.springframework.web.bind.annotation.RequestMapping;
3232
import org.springframework.web.context.request.ServletWebRequest;
33+
import org.springframework.web.cors.CorsUtils;
3334
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression;
3435

3536
/**
@@ -45,6 +46,9 @@
4546
*/
4647
public final class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> {
4748

49+
private final static ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition();
50+
51+
4852
private final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST =
4953
Collections.singletonList(new ProduceMediaTypeExpression("*/*"));
5054

@@ -176,6 +180,9 @@ public ProducesRequestCondition combine(ProducesRequestCondition other) {
176180
*/
177181
@Override
178182
public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) {
183+
if (CorsUtils.isPreFlightRequest(request)) {
184+
return PRE_FLIGHT_MATCH;
185+
}
179186
if (isEmpty()) {
180187
return this;
181188
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,46 +18,47 @@
1818

1919
import javax.servlet.http.HttpServletRequest;
2020

21-
import org.springframework.web.bind.annotation.RequestMapping;
22-
2321
/**
24-
* The contract for request conditions in Spring MVC's mapping infrastructure.
22+
* Contract for request mapping conditions.
2523
*
2624
* <p>Request conditions can be combined via {@link #combine(Object)}, matched to
2725
* a request via {@link #getMatchingCondition(HttpServletRequest)}, and compared
2826
* to each other via {@link #compareTo(Object, HttpServletRequest)} to determine
29-
* which matches a request more closely.
27+
* which is a closer match for a given request.
3028
*
3129
* @author Rossen Stoyanchev
3230
* @author Arjen Poutsma
3331
* @since 3.1
34-
* @param <T> the type of objects that this RequestCondition can be combined with and compared to
32+
* @param <T> the type of objects that this RequestCondition can be combined
33+
* with and compared to
3534
*/
3635
public interface RequestCondition<T> {
3736

3837
/**
39-
* Defines the rules for combining this condition (i.e. the current instance)
40-
* with another condition. For example combining type- and method-level
41-
* {@link RequestMapping} conditions.
38+
* Combine this condition with another such as conditions from a
39+
* type-level and method-level {@code @RequestMapping} annotation.
4240
* @param other the condition to combine with.
4341
* @return a request condition instance that is the result of combining
4442
* the two condition instances.
4543
*/
4644
T combine(T other);
4745

4846
/**
49-
* Checks if this condition matches the given request and returns a
50-
* potentially new request condition with content tailored to the
51-
* current request. For example a condition with URL patterns might
52-
* return a new condition that contains matching patterns sorted
53-
* with best matching patterns on top.
54-
* @return a condition instance in case of a match;
55-
* or {@code null} if there is no match
47+
* Check if the condition matches the request returning a potentially new
48+
* instance created for the current request. For example a condition with
49+
* multiple URL patterns may return a new instance only with those patterns
50+
* that match the request.
51+
* <p>For CORS pre-flight requests, conditions should match to the would-be,
52+
* actual request (e.g. URL pattern, query parameters, and the HTTP method
53+
* from the "Access-Control-Request-Method" header). If a condition cannot
54+
* be matched to a pre-flight request it should return an instance with
55+
* empty content thus not causing a failure to match.
56+
* @return a condition instance in case of a match or {@code null} otherwise.
5657
*/
5758
T getMatchingCondition(HttpServletRequest request);
5859

5960
/**
60-
* Compares this condition to another condition in the context of
61+
* Compare this condition to another condition in the context of
6162
* a specific request. This method assumes both instances have
6263
* been obtained via {@link #getMatchingCondition(HttpServletRequest)}
6364
* to ensure they have content relevant to current request only.

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

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import java.util.Set;
2525
import javax.servlet.http.HttpServletRequest;
2626

27+
import org.springframework.http.HttpHeaders;
2728
import org.springframework.web.bind.annotation.RequestMethod;
29+
import org.springframework.web.cors.CorsUtils;
2830

2931
/**
3032
* A logical disjunction (' || ') request condition that matches a request
@@ -101,12 +103,38 @@ public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition oth
101103
*/
102104
@Override
103105
public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) {
104-
RequestMethod requestMethod = getRequestMethod(request);
105-
if (this.methods.isEmpty()) {
106-
return (RequestMethod.OPTIONS.equals(requestMethod) ? null : this);
106+
107+
if (CorsUtils.isPreFlightRequest(request)) {
108+
return matchPreFlight(request);
109+
}
110+
111+
if (getMethods().isEmpty()) {
112+
if (RequestMethod.OPTIONS.name().equals(request.getMethod())) {
113+
return null; // No implicit match for OPTIONS (we handle it)
114+
}
115+
return this;
116+
}
117+
118+
return matchRequestMethod(request.getMethod());
119+
}
120+
121+
/**
122+
* On a pre-flight request match to the would-be, actual request.
123+
* Hence empty conditions is a match, otherwise try to match to the HTTP
124+
* method in the "Access-Control-Request-Method" header.
125+
*/
126+
private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) {
127+
if (getMethods().isEmpty()) {
128+
return this;
107129
}
130+
String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
131+
return matchRequestMethod(expectedMethod);
132+
}
133+
134+
private RequestMethodsRequestCondition matchRequestMethod(String httpMethod) {
135+
RequestMethod requestMethod = getRequestMethod(httpMethod);
108136
if (requestMethod != null) {
109-
for (RequestMethod method : this.methods) {
137+
for (RequestMethod method : getMethods()) {
110138
if (method.equals(requestMethod)) {
111139
return new RequestMethodsRequestCondition(method);
112140
}
@@ -118,9 +146,9 @@ public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest re
118146
return null;
119147
}
120148

121-
private RequestMethod getRequestMethod(HttpServletRequest request) {
149+
private RequestMethod getRequestMethod(String httpMethod) {
122150
try {
123-
return RequestMethod.valueOf(request.getMethod());
151+
return RequestMethod.valueOf(httpMethod);
124152
}
125153
catch (IllegalArgumentException ex) {
126154
return null;

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,7 @@ public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
215215
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
216216

217217
if (methods == null || params == null || headers == null || consumes == null || produces == null) {
218-
if (CorsUtils.isPreFlightRequest(request)) {
219-
methods = getAccessControlRequestMethodCondition(request);
220-
if (methods == null || params == null) {
221-
return null;
222-
}
223-
}
224-
else {
225-
return null;
226-
}
218+
return null;
227219
}
228220

229221
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
@@ -240,22 +232,6 @@ public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
240232
methods, params, headers, consumes, produces, custom.getCondition());
241233
}
242234

243-
/**
244-
* Return a matching RequestMethodsRequestCondition based on the expected
245-
* HTTP method specified in a CORS pre-flight request.
246-
*/
247-
private RequestMethodsRequestCondition getAccessControlRequestMethodCondition(HttpServletRequest request) {
248-
String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
249-
if (StringUtils.hasText(expectedMethod)) {
250-
for (RequestMethod method : getMethodsCondition().getMethods()) {
251-
if (expectedMethod.equalsIgnoreCase(method.name())) {
252-
return new RequestMethodsRequestCondition(method);
253-
}
254-
}
255-
}
256-
return null;
257-
}
258-
259235
/**
260236
* Compares "this" info (i.e. the current instance) with another info in the context of a request.
261237
* <p>Note: It is assumed both instances have been obtained via

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestConditionTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@
2222

2323
import org.junit.Test;
2424

25+
import org.springframework.http.HttpHeaders;
2526
import org.springframework.mock.web.test.MockHttpServletRequest;
2627
import org.springframework.web.bind.annotation.RequestMethod;
2728

2829
import static org.junit.Assert.assertEquals;
2930
import static org.junit.Assert.assertNotNull;
3031
import static org.junit.Assert.assertNull;
3132
import static org.junit.Assert.assertTrue;
33+
import static org.springframework.web.bind.annotation.RequestMethod.DELETE;
3234
import static org.springframework.web.bind.annotation.RequestMethod.GET;
3335
import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
3436
import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS;
3537
import static org.springframework.web.bind.annotation.RequestMethod.POST;
38+
import static org.springframework.web.bind.annotation.RequestMethod.PUT;
3639

3740
/**
3841
* @author Arjen Poutsma
@@ -73,6 +76,17 @@ public void getMatchingConditionWithCustomMethod() {
7376
assertNull(new RequestMethodsRequestCondition(GET, POST).getMatchingCondition(request));
7477
}
7578

79+
@Test
80+
public void getMatchingConditionWithCorsPreFlight() throws Exception {
81+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "");
82+
request.addHeader("Origin", "http://example.com");
83+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT");
84+
85+
assertNotNull(new RequestMethodsRequestCondition().getMatchingCondition(request));
86+
assertNotNull(new RequestMethodsRequestCondition(PUT).getMatchingCondition(request));
87+
assertNull(new RequestMethodsRequestCondition(DELETE).getMatchingCondition(request));
88+
}
89+
7690
@Test
7791
public void compareTo() {
7892
RequestMethodsRequestCondition c1 = new RequestMethodsRequestCondition(GET, HEAD);

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -332,7 +332,7 @@ public void preFlightRequest() {
332332
new PatternsRequestCondition("/foo"), new RequestMethodsRequestCondition(RequestMethod.OPTIONS), null,
333333
null, null, null, null);
334334
match = info.getMatchingCondition(request);
335-
assertNotNull(match);
335+
assertNull("Pre-flight should match the ACCESS_CONTROL_REQUEST_METHOD", match);
336336
}
337337

338338
}

0 commit comments

Comments
 (0)