From ba425941127296f07d8c90c26771843e9a0f6d34 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Sat, 8 Nov 2008 09:37:55 +0000 Subject: [PATCH] SPR-4927: Return 405 instead of 404 when HTTP method is not supported --- .../AnnotationMethodHandlerAdapter.java | 44 ++++++++++- .../ServletAnnotationControllerTests.java | 79 +++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 8bbc32d2088..37670bea4f3 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -24,10 +24,11 @@ import java.lang.reflect.Method; import java.security.Principal; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; - import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -51,7 +52,9 @@ import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; import org.springframework.validation.support.BindingAwareModelMap; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpSessionRequiredException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestDataBinder; @@ -333,6 +336,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen catch (NoSuchRequestHandlingMethodException ex) { return handleNoSuchRequestHandlingMethod(ex, request, response); } + catch (HttpRequestMethodNotSupportedException ex) { + return handleHttpRequestMethodNotSupportedException(ex, request, response); + } } public long getLastModified(HttpServletRequest request, Object handler) { @@ -360,6 +366,27 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return null; } + /** + * Handle the case where no request handler method was found for the particular HTTP request method. + * + *

The default implementation logs a warning, sends an HTTP 405 error and sets the "Allow" header. Alternatively, a + * fallback view could be chosen, or the HttpRequestMethodNotSupportedException could be rethrown as-is. + * + * @param ex the HttpRequestMethodNotSupportedException to be handled + * @param request current HTTP request + * @param response current HTTP response + * @return a ModelAndView to render, or null if handled directly + * @throws Exception an Exception that should be thrown as result of the servlet request + */ + protected ModelAndView handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex, + HttpServletRequest request, + HttpServletResponse response) throws Exception { + pageNotFoundLogger.warn(ex.getMessage()); + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + response.addHeader("Allow", StringUtils.arrayToDelimitedString(ex.getSupportedMethods(), ", ")); + return null; + } + /** * Template method for creating a new ServletRequestDataBinder instance. *

The default implementation creates a standard ServletRequestDataBinder. @@ -403,6 +430,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen String lookupPath = urlPathHelper.getLookupPathForRequest(request); Map targetHandlerMethods = new LinkedHashMap(); Map targetPathMatches = new LinkedHashMap(); + Set allowedMethods = new LinkedHashSet(7); String resolvedMethodName = null; for (Method handlerMethod : getHandlerMethods()) { RequestMappingInfo mappingInfo = new RequestMappingInfo(); @@ -423,6 +451,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen targetPathMatches.put(mappingInfo, mappedPath); } else { + for (RequestMethod requestMethod : mappingInfo.methods) { + allowedMethods.add(requestMethod.toString()); + } break; } } @@ -493,7 +524,14 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return targetHandlerMethods.get(bestMappingMatch); } else { - throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(), request.getParameterMap()); + if (!allowedMethods.isEmpty()) { + throw new HttpRequestMethodNotSupportedException(request.getMethod(), + StringUtils.toStringArray(allowedMethods)); + } + else { + throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(), + request.getParameterMap()); + } } } @@ -535,7 +573,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen private boolean responseArgumentUsed = false; - public ServletHandlerMethodInvoker(HandlerMethodResolver resolver) { + private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) { super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer, customArgumentResolvers); } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 92492cd2c99..91146b284e2 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -88,6 +88,7 @@ public class ServletAnnotationControllerTests { @Test public void standardHandleMethod() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyController.class)); @@ -106,6 +107,7 @@ public class ServletAnnotationControllerTests { @Test(expected = MissingServletRequestParameterException.class) public void requiredParamMissing() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(RequiredParamController.class)); @@ -123,6 +125,7 @@ public class ServletAnnotationControllerTests { @Test public void optionalParamMissing() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(OptionalParamController.class)); @@ -141,6 +144,7 @@ public class ServletAnnotationControllerTests { @Test public void defaultParamMissing() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(DefaultValueParamController.class)); @@ -156,9 +160,30 @@ public class ServletAnnotationControllerTests { assertEquals("foo", response.getContentAsString()); } + @Test + public void methodNotAllowed() throws Exception { + @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(MethodNotAllowedController.class)); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myPath.do"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals("Invalid response status", HttpServletResponse.SC_METHOD_NOT_ALLOWED, response.getStatus()); + assertEquals("Invalid Allow Header", "PUT, DELETE, HEAD, TRACE, OPTIONS, POST", response.getHeader("Allow")); + } + @Test public void proxiedStandardHandleMethod() throws Exception { DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyController.class)); @@ -182,6 +207,7 @@ public class ServletAnnotationControllerTests { @Test public void emptyParameterListHandleMethod() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -221,6 +247,7 @@ public class ServletAnnotationControllerTests { private void doTestAdaptedHandleMethods(final Class controllerClass) throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(controllerClass)); @@ -266,6 +293,7 @@ public class ServletAnnotationControllerTests { @Test public void formController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyFormController.class)); @@ -287,6 +315,7 @@ public class ServletAnnotationControllerTests { @Test public void modelFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyModelFormController.class)); @@ -308,6 +337,7 @@ public class ServletAnnotationControllerTests { @Test public void proxiedFormController() throws Exception { DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyFormController.class)); @@ -334,6 +364,7 @@ public class ServletAnnotationControllerTests { @Test public void commandProvidingFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -360,6 +391,7 @@ public class ServletAnnotationControllerTests { @Test public void typedCommandProvidingFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -404,6 +436,7 @@ public class ServletAnnotationControllerTests { @Test public void binderInitializingCommandProvidingFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -427,6 +460,7 @@ public class ServletAnnotationControllerTests { @Test public void specificBinderInitializingCommandProvidingFormController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -453,6 +487,7 @@ public class ServletAnnotationControllerTests { final MockServletConfig servletConfig = new MockServletConfig(servletContext); @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.setServletContext(servletContext); @@ -510,6 +545,7 @@ public class ServletAnnotationControllerTests { @Test public void methodNameDispatchingController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MethodNameDispatchingController.class)); @@ -543,6 +579,7 @@ public class ServletAnnotationControllerTests { @Test public void methodNameDispatchingControllerWithSuffix() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MethodNameDispatchingController.class)); @@ -581,6 +618,7 @@ public class ServletAnnotationControllerTests { @Test public void controllerClassNamePlusMethodNameDispatchingController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); RootBeanDefinition mapping = new RootBeanDefinition(ControllerClassNameHandlerMapping.class); @@ -617,6 +655,7 @@ public class ServletAnnotationControllerTests { @Test public void postMethodNameDispatchingController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -666,6 +705,7 @@ public class ServletAnnotationControllerTests { @Test public void relativePathDispatchingController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", @@ -700,6 +740,7 @@ public class ServletAnnotationControllerTests { @Test public void nullCommandController() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(MyNullCommandController.class)); @@ -719,6 +760,7 @@ public class ServletAnnotationControllerTests { @Test public void equivalentMappingsWithSameMethodName() throws Exception { @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); wac.registerBeanDefinition("controller", new RootBeanDefinition(ChildController.class)); @@ -743,6 +785,7 @@ public class ServletAnnotationControllerTests { @RequestMapping("/myPath.do") private static class MyController extends AbstractController { + @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { response.getWriter().write("test"); @@ -750,6 +793,7 @@ public class ServletAnnotationControllerTests { } } + /** @noinspection UnusedDeclaration*/ private static class BaseController { @RequestMapping(method = RequestMethod.GET) @@ -851,10 +895,12 @@ public class ServletAnnotationControllerTests { response.getWriter().write("test-" + tb.getName() + "-" + errors.getFieldError("age").getCode()); } + @Override @InitBinder public void initBinder(@RequestParam("param1")String p1, int param2) { } + @Override @ModelAttribute public void modelAttribute(@RequestParam("param1")String p1, int param2) { } @@ -928,6 +974,7 @@ public class ServletAnnotationControllerTests { return new TestBean(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); } + @Override @RequestMapping("/myPath.do") public String myHandle(@ModelAttribute("myCommand")TestBean tb, BindingResult errors, ModelMap model) { return super.myHandle(tb, errors, model); @@ -1221,4 +1268,36 @@ public class ServletAnnotationControllerTests { } } + @Controller + public static class MethodNotAllowedController { + + + @RequestMapping(value="/myPath.do", method = RequestMethod.DELETE) + public void delete() { + } + + @RequestMapping(value="/myPath.do", method = RequestMethod.HEAD) + public void head() { + } + + @RequestMapping(value="/myPath.do", method = RequestMethod.OPTIONS) + public void options() { + } + @RequestMapping(value="/myPath.do", method = RequestMethod.POST) + public void post() { + } + + @RequestMapping(value="/myPath.do", method = RequestMethod.PUT) + public void put() { + } + + @RequestMapping(value="/myPath.do", method = RequestMethod.TRACE) + public void trace() { + } + + @RequestMapping(value="/otherPath.do", method = RequestMethod.GET) + public void get() { + } + } + }