javaspringspring-boottomcat

Spring Boot web app is processing incorrect POST requests after Spring/Spring Boot upgrade


I am upgrading the dependencies of a spring boot application with an embedded Tomcat server. When stepping through and upgrading from Spring 5.1 -> 5.2 and Spring Boot 2.1 -> 2.2 an issue arose where only POST and GET Http requests were being processed, while PATCH and DELETE were not. The Controller and JSP pages worked perfectly until I made this Spring upgrade.

For instance, when I breakpoint the below controller I can trace myself going to the Edit page. When I click Submit on that page it should then take me to the controller's update() method with RequestMethod.PATCH annotated. Since the upgrade however this instead takes me to the controller's create() method with RequestMethod.POST instead. It will then error because it's trying to add an existing item in the database, instead of updating an existing one. This is happening for multiple controllers.

I've confirmed that the normal use case of going to the Add page and clicking submit will hit the correct create() method in the controller and will create a new item.

I am required to use JAVA 8 and have pushed both Spring and Boot as far forward in versions as I can hoping this would resolve itself, but the issue remains. My current dependency versions are below.

I've attempted to breakpoint through the relevant Spring and Tomcat classes I can find to see where the error is occurring. I've confirmed that the startup building of Spring's RequestMappingInfoHandlerMapping is correctly mapping the controller's handler methods to the HTTP methods. Instead, it seems to be that the HttpServletRequest objects sent to Spring already have the incorrect HTTP methods. I've attempted to breakpoint through Tomcat classes to investigate, but am at a loss as to what is causing this, let alone how to resolve it.

Just in case I cleared out the .m2\repository folder and reloaded Maven, but to no avail.

Parent Pom.xml

<spring.version>5.3.39</spring.version>
<spring-boot.version>2.7.18</spring-boot.version>
<spring-security.version>5.8.16</spring-security.version>
<tomcat.version>9.0.102</tomcat.version>
    
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>${spring-boot.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <version>${spring-boot.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>${spring-boot.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring-boot.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>${spring-boot.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
    <version>${spring-boot.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons</artifactId>
    <version>2.1.15.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.1.15.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring-security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>${spring-security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
    <version>${spring-security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>${spring-security.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring-security.version}</version>
</dependency>

Sub-Pom1.xml

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>${tomcat.version}</version>
</dependency>

Sub-Pom2.xml

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-el-api</artifactId>
    <version>${tomcat.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>${tomcat.version}</version>
    <scope>test</scope>
</dependency>

Configuration.java

@Configuration
@Import(CoreConfiguration.class)
@EnableAutoConfiguration
@ComponentScan("org.aurora.biorepository")
public class Configuration implements WebMvcConfigurer {
    public static void main(String[] args) throws Exception {
        System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, "not_test");
        SpringApplication.run(ApplicationConfiguration.class, args);
    }


    @Bean
    public ServletWebServerFactory embeddedServletContainerFactory() {
        return new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                ((StandardJarScanner) context.getJarScanner()).setScanManifest(false);
            }
        };
    }


    @Bean
    public FilterRegistrationBean getFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(getMethodConvertingFilter());
        registration.addUrlPatterns("/*");
        registration.setDispatcherTypes(DispatcherType.FORWARD);
        registration.setName("getMethodConvertingFilter");
        return registration;
    }

    @Bean
    public GetMethodConvertingFilter getMethodConvertingFilter() {
        return new GetMethodConvertingFilter();
    }

    @Bean
    @Autowired
    public DomainClassConverter domainClassConverter(@Qualifier("mvcConversionService") ConversionService cs) {
        return new DomainClassConverter(cs);
    }
}   

GetMethodConvertingFilter.java

public class GetMethodConvertingFilter implements Filter {

    @Override
    public void init(FilterConfig config) throws ServletException {
        // do nothing
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        chain.doFilter(wrapRequest((HttpServletRequest) request), response);
    }

    @Override
    public void destroy() {
        // do nothing
    }

    private static HttpServletRequestWrapper wrapRequest(HttpServletRequest request) {
        return new HttpServletRequestWrapper(request) {
            @Override
            public String getMethod() {
                return "GET";
            }
        };
    }
}

Controller.java

@Controller
@RequestMapping("/home")
public class Controller {

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String add(@ModelAttribute("modelAttribute") AddForm addForm, Errors errors, Model model, RedirectAttributes redirectAttributes) {
        ...doStuff();

        return "home/add";
    }
    
    @RequestMapping(method = RequestMethod.POST)
    public String create(@ModelAttribute("modelAttribute") @Valid AddForm addForm, Errors errors, Model model, RedirectAttributes redirectAttributes, javax.servlet.http.HttpServletRequest request) {
        ...doStuff();
    }
    
    @RequestMapping(value = "/{key}/edit", method = RequestMethod.GET)
    public String edit(ModelMap model, @PathVariable String key) {
        ...doStuff();
        
        return "home/edit";
    }
    
    @RequestMapping(method = RequestMethod.PATCH)
    public String update(@ModelAttribute("modelAttribute") @Valid AddForm addForm, Errors errors, Model model, RedirectAttributes redirectAttributes, HttpServletRequest request) throws ObjectNotFoundException {
        ...doStuff();
    }
}   

Add.jsp

<tiles:insertDefinition name="base">
    <tiles:putAttribute name="title"><fmt:message key="org.home.Controller.add.title" /></tiles:putAttribute>
    <tiles:putAttribute name="content">
        <form:form action="/home" method="POST" modelAttribute="modelAttribute">
            <div class="half-page">             
                <%@ include file="_form.jsp" %>
                <div class="row">
                    <input type="submit" name="submit" value="<fmt:message key="org.home.Controller.add.action.submit" />" class="button" />
                </div>
            </div>
        </form:form>
    </tiles:putAttribute>
    <tiles:putAttribute name="javascripts">
        <script type="text/javascript" src="<c:url value="/resources/js/home/_form.js" />"></script>
    </tiles:putAttribute>
</tiles:insertDefinition>

Edit.jsp

<tiles:insertDefinition name="base">
    <tiles:putAttribute name="title">
        <fmt:message key="org.home.Controller.edit.title">
            <fmt:param value="${itemAddForm.item.name}" />
        </fmt:message>
    </tiles:putAttribute>
    <tiles:putAttribute name="sectionNavigation" type="template" value="/WEB-INF/views/home/" />
    <tiles:putAttribute name="content">
        <form:form action="/home" method="PATCH" modelAttribute="modelAttribute">
            <div class="half-page">
                <%@ include file="_form.jsp" %>
                <div class="row">
                    <input type="submit" name="submit" value="<fmt:message key="org.home.Controller.edit.action.submit" />" class="button" />
                </div>
            </div>
        </form:form>
    </tiles:putAttribute>
    <tiles:putAttribute name="javascripts">
        <script type="text/javascript" src="<c:url value="/resources/js/home/_form.js" />"></script>
    </tiles:putAttribute>
</tiles:insertDefinition>

Solution

  • My team eventually figured this out. Removing the GetMethodConvertingFilter did not have an effect, but we needed another filter instead. Adding a new bean to our Configuration class for HiddenHttpMethodFilter was the answer. This required some minor changes to our Edit.jsp pages as well. This allowed Spring to process PATCH requests. Below are the changes.

    Configuration.java

    @Bean
        public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
            return new HiddenHttpMethodFilter();
        }
    

    Edit.jsp Replace:

    <form:form action="/home" method="PATCH" modelAttribute="modelAttribute">
    

    With:

    <form:form action="/home" method="POST" modelAttribute="modelAttribute">
     <input type="hidden" name="_method" value="PATCH" />