Description
Spring Boot version: 3.2.3, 3.2.4 (Spring Core 6.1.4)
Java version: Temurin 17 (17.0.10)
Reproduced on Windows and Github Actions with Ubuntu 22.04.
Minimal example: https://github.com/RHarryH/spring-webmvc-github-issue
Description:
I have observed weird issue of WebMvcTest
failure with code 405
instead of expected 200
because Spring does not resolve controller method based on the request url. 405
error happens when there are two endpoints with the same request url but different HTTP method. When request urls are different 404
error is thrown.
This happens only when specific hierarchy of controllers is used and when WebMvcTest
is run after SpringBootTest
(achieved by changing test class execution order in junit-platform.properties
.
The hierarchy of the controllers is as follows:
Controller
interface defining endpoints and annotating them with@XMapping
annotationsAbstractController
implementingdelete
method. Please not it is a package-private abstract classActualController
implementing remaining methods
The presence of AbstractController
is the main cause of the issue. Working workaround is making it public
.
When debugging tests SpringBootTest
logs contains:
2024-04-07T12:01:20.781+02:00 DEBUG 33568 --- [ main] _.s.web.servlet.HandlerMapping.Mappings :
c.a.i.ActualController:
{POST [/v1/a]}: add(Body,BindingResult)
{POST [/v1/a/{id}]}: update(UUID,Body,BindingResult)
{DELETE [/v1/a/{id}]}: delete(UUID)
while WebMvcTest
logs miss DELETE
method:
2024-04-07T12:01:22.203+02:00 DEBUG 33568 --- [ main] _.s.web.servlet.HandlerMapping.Mappings :
c.a.i.ActualController:
{POST [/v1/a]}: add(Body,BindingResult)
{POST [/v1/a/{id}]}: update(UUID,Body,BindingResult)
I have tracked down the rootcause to the org.springframework.core.MethodIntrocpector
class and selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup)
method (
Line 74 correctly inspects the method. The problem is in line 77. When SpringBootTest
tests are run the fields looks like below:
method = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
specificMethod = {Method@7493} "public void com.avispa.issue.ActualController.delete(java.util.UUID)"
result = {RequestMappingInfo@7494} "{DELETE [/v1/a/{id}]}"
bridgedMethod = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
But then whenWebMvcTest
tests are run it looks like below:
method = {Method@9155} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
specificMethod = {Method@9156} "public void com.avispa.issue.ActualController.delete(java.util.UUID)"
result = {RequestMappingInfo@9157} "{DELETE [/v1/a/{id}]}"
bridgedMethod = {Method@7492} "public void com.avispa.issue.AbstractController.delete(java.util.UUID)"
As you can see in second case method
and bridgedMethod
represents the same method but are in fact different instances of Method
class. And because the comparison in line 77 is done by reference, it failes and does not add found DELETE
method to the mappings registry.
When SpringBootTest
tests are disabled, the problem does not exist.