GH-3366: Return null from HTTP handleNoMatch

Fixes: https://github.com/spring-projects/spring-integration/issues/3366

When the same path is mapped for integration HTTP endpoint and MVC
method mapping, but different other mapping options (e.g. method)
and one of them fails to match, there is no way to try other
`RequestMapping` from the `DispatcherServlet` because
`RequestMappingHandlerMapping.handleNoMatch()` throws an exception
when no match instead of `null` according chain of responsibility logic
in the `DispatcherServlet`

*  Rework `IntegrationRequestMappingHandlerMapping.handleNoMatch()` to catch
all the super exception and return `null` to the `DispatcherServlet` to let
it to try other `RequestMapping` from the configuration
* Change an order for `IntegrationRequestMappingHandlerMapping` to `-1`
to let it to be tried first before regular MVC `RequestMappingHandlerMapping`
* Add a test-case to ensure that mix-in Integration HTTP and MVC for the
same path works as expected without failing on first try
This commit is contained in:
Artem Bilan
2020-09-10 12:12:45 -04:00
committed by Gary Russell
parent 09dec2eab5
commit 9aa9707f37
4 changed files with 50 additions and 16 deletions

View File

@@ -55,8 +55,7 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
* which could also be overridden by the user by simply registering
* a {@link IntegrationRequestMappingHandlerMapping} {@code <bean>} with 'id'
* {@link HttpContextUtils#HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In addition, checks if the {@code javax.servlet.Servlet} class is present on the classpath.
* <p> In addition, checks if the {@code javax.servlet.Servlet} class is present on the classpath.
* When Spring Integration HTTP is used only as an HTTP client, there is no reason to use and register
* the HTTP server components.
*/
@@ -64,9 +63,9 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
if (HttpContextUtils.WEB_MVC_PRESENT &&
!registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) {
BeanDefinitionBuilder requestMappingBuilder =
BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class);
requestMappingBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
requestMappingBuilder.addPropertyValue(IntegrationNamespaceUtils.ORDER, 0);
BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
.addPropertyValue(IntegrationNamespaceUtils.ORDER, -1);
registry.registerBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME,
requestMappingBuilder.getBeanDefinition());
}

View File

@@ -21,8 +21,10 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -177,6 +179,18 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin
return null;
}
@Override
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String path, HttpServletRequest request) {
try {
return super.handleNoMatch(infos, path, request);
}
catch (ServletException ex) {
// Since this component has a higher precedence the 'null' return allows a
// 'DispatcherServlet' to try other 'HandlerMapping'
return null;
}
}
private static CorsConfiguration buildCorsConfiguration(CrossOrigin crossOrigin, RequestMappingInfo mappingInfo) {
CorsConfiguration config = new CorsConfiguration();
for (RequestMethod requestMethod : crossOrigin.getMethod()) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
package org.springframework.integration.http.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willReturn;
@@ -56,7 +55,6 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.HandlerMapping;
@@ -195,18 +193,13 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes
}
@Test
public void getRequestNotAllowed() {
public void getRequestNotAllowed() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.setParameter("foo", "bar");
request.setRequestURI("/postOnly");
assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class)
.isThrownBy(() -> this.integrationRequestMappingHandlerMapping.getHandler(request))
.satisfies((ex) -> {
assertThat(ex.getMethod()).isEqualTo("GET");
assertThat(ex.getSupportedMethods()).containsExactly("POST");
});
assertThat(this.integrationRequestMappingHandlerMapping.getHandler(request)).isNull();
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -78,6 +78,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.WebApplicationContext;
@@ -85,6 +87,7 @@ import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* @author Artem Bilan
@@ -208,6 +211,7 @@ public class HttpDslTests {
}
@Autowired
@Qualifier("customValidator")
private Validator validator;
@Test
@@ -293,9 +297,20 @@ public class HttpDslTests {
flowRegistration.destroy();
}
@Test
public void testMixWithMvcRequestMapping() throws Exception {
this.mockMvc.perform(
get("/mvcRequest")
.with(httpBasic("user", "user")))
.andExpect(status().isOk())
.andExpect(content().string("MVC reply"));
}
@Configuration
@EnableWebSecurity
@EnableIntegration
@EnableWebMvc
@RestController
public static class ContextConfiguration extends WebSecurityConfigurerAdapter {
@Override
@@ -409,6 +424,19 @@ public class HttpDslTests {
return new TestModelValidator();
}
@GetMapping("/mvcRequest")
ResponseEntity<?> mvcGet() {
return ResponseEntity.ok("MVC reply");
}
@Bean
HttpRequestHandlerEndpointSpec mvcHandler() {
return Http.inboundChannelAdapter("/mvcRequest")
.requestMapping((mapping) -> mapping.methods(HttpMethod.POST));
}
}
public static class HttpProxyResponseErrorHandler extends DefaultResponseErrorHandler {