Skip to content

Commit ccd17df

Browse files
committed
Support HTTP OPTIONS
Issue: SPR-13130
1 parent d70ad76 commit ccd17df

12 files changed

+179
-29
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public class DispatcherServlet extends FrameworkServlet {
347347
*/
348348
public DispatcherServlet() {
349349
super();
350+
this.setDispatchOptionsRequest(true);
350351
}
351352

352353
/**
@@ -390,6 +391,7 @@ public DispatcherServlet() {
390391
*/
391392
public DispatcherServlet(WebApplicationContext webApplicationContext) {
392393
super(webApplicationContext);
394+
this.setDispatchOptionsRequest(true);
393395
}
394396

395397
/**

spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java

Lines changed: 6 additions & 4 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.
@@ -428,9 +428,11 @@ public void setThreadContextInheritable(boolean threadContextInheritable) {
428428
/**
429429
* Set whether this servlet should dispatch an HTTP OPTIONS request to
430430
* the {@link #doService} method.
431-
* <p>Default is "false", applying {@link javax.servlet.http.HttpServlet}'s
432-
* default behavior (i.e. enumerating all standard HTTP request methods
433-
* as a response to the OPTIONS request).
431+
* <p>Default in the {@code FrameworkServlet} is "false", applying
432+
* {@link javax.servlet.http.HttpServlet}'s default behavior (i.e.enumerating
433+
* all standard HTTP request methods as a response to the OPTIONS request).
434+
* Note however that as of 4.3 the {@code DispatcherServlet} sets this
435+
* property to "true" by default due to its built-in support for OPTIONS.
434436
* <p>Turn this flag on if you prefer OPTIONS requests to go through the
435437
* regular dispatching chain, just like other HTTP requests. This usually
436438
* means that your controllers will receive those requests; make sure

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java

Lines changed: 5 additions & 3 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.
@@ -30,6 +30,7 @@
3030
*
3131
* @author Juergen Hoeller
3232
* @author Arjen Poutsma
33+
* @author Rossen Stoyanchev
3334
* @since 2.5.2
3435
* @deprecated as of Spring 3.2, together with {@link DefaultAnnotationHandlerMapping},
3536
* {@link AnnotationMethodHandlerAdapter}, and {@link AnnotationMethodHandlerExceptionResolver}.
@@ -43,11 +44,12 @@ abstract class ServletAnnotationMappingUtils {
4344
* @param request the current HTTP request to check
4445
*/
4546
public static boolean checkRequestMethod(RequestMethod[] methods, HttpServletRequest request) {
46-
if (ObjectUtils.isEmpty(methods)) {
47+
String inputMethod = request.getMethod();
48+
if (ObjectUtils.isEmpty(methods) && !RequestMethod.OPTIONS.name().equals(inputMethod)) {
4749
return true;
4850
}
4951
for (RequestMethod method : methods) {
50-
if (method.name().equals(request.getMethod())) {
52+
if (method.name().equals(inputMethod)) {
5153
return true;
5254
}
5355
}

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

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,24 @@ public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition oth
9999
*/
100100
@Override
101101
public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) {
102+
RequestMethod requestMethod = getRequestMethod(request);
103+
if (requestMethod == null) {
104+
return null;
105+
}
102106
if (this.methods.isEmpty()) {
103-
return this;
107+
return (RequestMethod.OPTIONS.equals(requestMethod) ? null : this);
104108
}
105-
RequestMethod requestMethod = getRequestMethod(request);
106-
if (requestMethod != null) {
107-
for (RequestMethod method : this.methods) {
108-
if (method.equals(requestMethod)) {
109-
return new RequestMethodsRequestCondition(method);
110-
}
111-
}
112-
if (isHeadRequest(requestMethod) && getMethods().contains(RequestMethod.GET)) {
113-
return HEAD_CONDITION;
109+
for (RequestMethod method : this.methods) {
110+
if (method.equals(requestMethod)) {
111+
return new RequestMethodsRequestCondition(method);
114112
}
115113
}
114+
if (RequestMethod.HEAD.equals(requestMethod) && getMethods().contains(RequestMethod.GET)) {
115+
return HEAD_CONDITION;
116+
}
116117
return null;
117118
}
118119

119-
private boolean isHeadRequest(RequestMethod requestMethod) {
120-
return (requestMethod != null && RequestMethod.HEAD.equals(requestMethod));
121-
}
122-
123120
private RequestMethod getRequestMethod(HttpServletRequest request) {
124121
try {
125122
return RequestMethod.valueOf(request.getMethod());

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

Lines changed: 61 additions & 3 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.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.mvc.method;
1818

19+
import java.lang.reflect.Method;
1920
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.Comparator;
@@ -29,6 +30,8 @@
2930
import javax.servlet.ServletException;
3031
import javax.servlet.http.HttpServletRequest;
3132

33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpMethod;
3235
import org.springframework.http.InvalidMediaTypeException;
3336
import org.springframework.http.MediaType;
3437
import org.springframework.util.CollectionUtils;
@@ -56,6 +59,19 @@
5659
*/
5760
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
5861

62+
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
63+
64+
static {
65+
try {
66+
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
67+
}
68+
catch (NoSuchMethodException ex) {
69+
// Should never happen
70+
throw new IllegalStateException("No handler for HTTP OPTIONS", ex);
71+
}
72+
}
73+
74+
5975
protected RequestMappingInfoHandlerMapping() {
6076
setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
6177
}
@@ -200,8 +216,14 @@ protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfo
200216
if (patternMatches.isEmpty()) {
201217
return null;
202218
}
203-
else if (patternAndMethodMatches.isEmpty() && !allowedMethods.isEmpty()) {
204-
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
219+
else if (patternAndMethodMatches.isEmpty()) {
220+
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
221+
HttpOptionsHandler handler = new HttpOptionsHandler(allowedMethods);
222+
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
223+
}
224+
else if (!allowedMethods.isEmpty()) {
225+
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
226+
}
205227
}
206228

207229
Set<MediaType> consumableMediaTypes;
@@ -279,4 +301,40 @@ private List<String[]> getRequestParams(HttpServletRequest request, Set<RequestM
279301
return result;
280302
}
281303

304+
305+
/**
306+
* Default handler for HTTP OPTIONS.
307+
*/
308+
private static class HttpOptionsHandler {
309+
310+
private final HttpHeaders headers = new HttpHeaders();
311+
312+
313+
public HttpOptionsHandler(Set<String> declaredMethods) {
314+
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
315+
}
316+
317+
private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) {
318+
Set<HttpMethod> result = new LinkedHashSet<HttpMethod>(declaredMethods.size());
319+
if (declaredMethods.isEmpty()) {
320+
result.add(HttpMethod.GET);
321+
result.add(HttpMethod.HEAD);
322+
}
323+
else {
324+
boolean hasHead = declaredMethods.contains("HEAD");
325+
for (String method : declaredMethods) {
326+
result.add(HttpMethod.valueOf(method));
327+
if (!hasHead && "GET".equals(method)) {
328+
result.add(HttpMethod.HEAD);
329+
}
330+
}
331+
}
332+
return result;
333+
}
334+
335+
public HttpHeaders handle() {
336+
return this.headers;
337+
}
338+
}
339+
282340
}

spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.core.io.ClassPathResource;
3838
import org.springframework.core.io.Resource;
3939
import org.springframework.http.HttpHeaders;
40+
import org.springframework.http.HttpMethod;
4041
import org.springframework.http.HttpRange;
4142
import org.springframework.http.MediaType;
4243
import org.springframework.http.server.ServletServerHttpRequest;
@@ -111,7 +112,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
111112

112113

113114
public ResourceHttpRequestHandler() {
114-
super(METHOD_GET, METHOD_HEAD);
115+
super(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.OPTIONS.name());
115116
this.resourceResolvers.add(new PathResourceResolver());
116117
}
117118

@@ -236,6 +237,11 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
236237
return;
237238
}
238239

240+
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
241+
response.setHeader("Allow", "GET,HEAD");
242+
return;
243+
}
244+
239245
// Header phase
240246
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
241247
logger.trace("Resource not modified - returning 304");

spring-webmvc/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java

Lines changed: 2 additions & 1 deletion
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.
@@ -862,6 +862,7 @@ public void allowedOptionsIncludesPatchMethod() throws Exception {
862862
MockHttpServletRequest request = new MockHttpServletRequest(getServletContext(), "OPTIONS", "/foo");
863863
MockHttpServletResponse response = spy(new MockHttpServletResponse());
864864
DispatcherServlet servlet = new DispatcherServlet();
865+
servlet.setDispatchOptionsRequest(false);
865866
servlet.service(request, response);
866867
verify(response, never()).getHeader(anyString()); // SPR-10341
867868
assertThat(response.getHeader("Allow"), equalTo("GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH"));

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java

Lines changed: 12 additions & 1 deletion
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.
@@ -1966,6 +1966,17 @@ protected WebApplicationContext createWebApplicationContext(WebApplicationContex
19661966
assertEquals("1-2", response.getContentAsString());
19671967
}
19681968

1969+
@Test
1970+
public void httpOptions() throws ServletException, IOException {
1971+
initServlet(ResponseEntityController.class);
1972+
1973+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/foo");
1974+
MockHttpServletResponse response = new MockHttpServletResponse();
1975+
servlet.service(request, response);
1976+
assertEquals(404, response.getStatus());
1977+
}
1978+
1979+
19691980
public static class ListEditorRegistrar implements PropertyEditorRegistrar {
19701981

19711982
@Override

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ public void methodHeadNoMatch() throws Exception {
8282
}
8383

8484
@Test
85-
public void noDeclaredMethodsMatchesAllMethods() {
85+
public void noDeclaredMethodsMatchesAllMethodsExceptOptions() {
8686
RequestCondition condition = new RequestMethodsRequestCondition();
8787

8888
assertNotNull(condition.getMatchingCondition(new MockHttpServletRequest("GET", "")));
8989
assertNotNull(condition.getMatchingCondition(new MockHttpServletRequest("POST", "")));
9090
assertNotNull(condition.getMatchingCondition(new MockHttpServletRequest("HEAD", "")));
91+
assertNull(condition.getMatchingCondition(new MockHttpServletRequest("OPTIONS", "")));
9192
}
9293

9394
@Test

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.junit.Test;
3434

3535
import org.springframework.core.annotation.AnnotationUtils;
36+
import org.springframework.http.HttpHeaders;
3637
import org.springframework.http.MediaType;
3738
import org.springframework.mock.web.test.MockHttpServletRequest;
3839
import org.springframework.stereotype.Controller;
@@ -44,8 +45,11 @@
4445
import org.springframework.web.bind.annotation.RequestBody;
4546
import org.springframework.web.bind.annotation.RequestMapping;
4647
import org.springframework.web.bind.annotation.RequestMethod;
48+
import org.springframework.web.context.request.ServletWebRequest;
4749
import org.springframework.web.context.support.StaticWebApplicationContext;
4850
import org.springframework.web.method.HandlerMethod;
51+
import org.springframework.web.method.support.InvocableHandlerMethod;
52+
import org.springframework.web.method.support.ModelAndViewContainer;
4953
import org.springframework.web.servlet.HandlerExecutionChain;
5054
import org.springframework.web.servlet.HandlerInterceptor;
5155
import org.springframework.web.servlet.HandlerMapping;
@@ -172,6 +176,14 @@ public void getHandlerMediaTypeNotSupported() throws Exception {
172176
testHttpMediaTypeNotSupportedException("/person/1.json");
173177
}
174178

179+
@Test
180+
public void getHandlerHttpOptions() throws Exception {
181+
testHttpOptions("/foo", "GET,HEAD");
182+
testHttpOptions("/person/1", "PUT");
183+
testHttpOptions("/persons", "GET,HEAD");
184+
testHttpOptions("/something", "PUT,POST");
185+
}
186+
175187
@Test
176188
public void getHandlerTestInvalidContentType() throws Exception {
177189
try {
@@ -388,6 +400,19 @@ private void testHttpMediaTypeNotSupportedException(String url) throws Exception
388400
}
389401
}
390402

403+
private void testHttpOptions(String requestURI, String allowHeader) throws Exception {
404+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", requestURI);
405+
HandlerMethod handlerMethod = getHandler(request);
406+
407+
ServletWebRequest webRequest = new ServletWebRequest(request);
408+
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
409+
Object result = new InvocableHandlerMethod(handlerMethod).invokeForRequest(webRequest, mavContainer);
410+
411+
assertNotNull(result);
412+
assertEquals(HttpHeaders.class, result.getClass());
413+
assertEquals(allowHeader, ((HttpHeaders) result).getFirst("Allow"));
414+
}
415+
391416
private void testHttpMediaTypeNotAcceptableException(String url) throws Exception {
392417
try {
393418
MockHttpServletRequest request = new MockHttpServletRequest("GET", url);
@@ -468,6 +493,13 @@ public String xmlContent() {
468493
public String nonXmlContent() {
469494
return "";
470495
}
496+
497+
@RequestMapping(value = "/something", method = RequestMethod.OPTIONS)
498+
public HttpHeaders fooOptions() {
499+
HttpHeaders headers = new HttpHeaders();
500+
headers.add("Allow", "PUT,POST");
501+
return headers;
502+
}
471503
}
472504

473505
@SuppressWarnings("unused")

0 commit comments

Comments
 (0)