Skip to content

Commit 5035306

Browse files
Renato Mamelirenatomameli
authored andcommitted
Fix consumes handling for interface @RequestBody
Previously, @RequestBody(required = false) annotations declared on interface methods were ignored when resolving the consumes condition. This caused mappings to incorrectly require a request body with a Content-Type such as application/json, even when no body was provided. This change uses AnnotatedMethod to retrieve parameter annotations from both the implementation and its interfaces, ensuring that the required flag is respected and body presence is evaluated correctly. Signed-off-by: Renato Mameli <[email protected]>
1 parent a265a13 commit 5035306

File tree

2 files changed

+75
-8
lines changed

2 files changed

+75
-8
lines changed

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22-
import java.lang.reflect.Parameter;
2322
import java.util.Collections;
2423
import java.util.LinkedHashMap;
2524
import java.util.List;
@@ -31,7 +30,9 @@
3130
import org.jspecify.annotations.Nullable;
3231

3332
import org.springframework.context.EmbeddedValueResolverAware;
33+
import org.springframework.core.MethodParameter;
3434
import org.springframework.core.annotation.AnnotatedElementUtils;
35+
import org.springframework.core.annotation.AnnotatedMethod;
3536
import org.springframework.core.annotation.MergedAnnotation;
3637
import org.springframework.core.annotation.MergedAnnotationPredicates;
3738
import org.springframework.core.annotation.MergedAnnotations;
@@ -410,13 +411,17 @@ protected void registerHandlerMethod(Object handler, Method method, RequestMappi
410411

411412
private void updateConsumesCondition(RequestMappingInfo info, Method method) {
412413
ConsumesRequestCondition condition = info.getConsumesCondition();
413-
if (!condition.isEmpty()) {
414-
for (Parameter parameter : method.getParameters()) {
415-
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
416-
if (annot.isPresent()) {
417-
condition.setBodyRequired(annot.getBoolean("required"));
418-
break;
419-
}
414+
if (condition.isEmpty()) {
415+
return;
416+
}
417+
418+
AnnotatedMethod annotatedMethod = new AnnotatedMethod(method);
419+
420+
for (MethodParameter parameter : annotatedMethod.getMethodParameters()) {
421+
RequestBody requestBodyAnnotation = parameter.getParameterAnnotation(RequestBody.class);
422+
if (requestBodyAnnotation != null) {
423+
condition.setBodyRequired(requestBodyAnnotation.required());
424+
break;
420425
}
421426
}
422427
}

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,52 @@ void httpExchangeWithCustomHeaders() throws Exception {
386386
.containsExactly("h1=hv1", "!h2");
387387
}
388388

389+
@Test
390+
void requestBodyAnnotationFromInterfaceIsRespected() throws Exception {
391+
RequestMappingHandlerMapping mapping = createMapping();
392+
393+
Class<?> controllerClass = InterfaceControllerImpl.class;
394+
Method method = controllerClass.getDeclaredMethod("post", Foo.class);
395+
396+
RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
397+
assertThat(info).isNotNull();
398+
399+
mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);
400+
401+
assertThat(info.getConsumesCondition()).isNotNull();
402+
assertThat(info.getConsumesCondition().isBodyRequired()).isFalse();
403+
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);
404+
405+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
406+
initRequestPath(mapping, request);
407+
408+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
409+
assertThat(matchingInfo).isNotNull();
410+
}
411+
412+
@Test
413+
void requestBodyAnnotationFromImplementationOverridesInterface() throws Exception {
414+
RequestMappingHandlerMapping mapping = createMapping();
415+
416+
Class<?> controllerClass = InterfaceControllerImplOverridesRequestBody.class;
417+
Method method = controllerClass.getDeclaredMethod("post", Foo.class);
418+
419+
RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
420+
assertThat(info).isNotNull();
421+
422+
mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);
423+
424+
assertThat(info.getConsumesCondition()).isNotNull();
425+
assertThat(info.getConsumesCondition().isBodyRequired()).isTrue();
426+
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);
427+
428+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
429+
initRequestPath(mapping, request);
430+
431+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
432+
assertThat(matchingInfo).isNull();
433+
}
434+
389435
private static RequestMappingHandlerMapping createMapping() {
390436
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
391437
mapping.setApplicationContext(new StaticWebApplicationContext());
@@ -571,6 +617,22 @@ static class MethodLevelOverriddenHttpExchangeAnnotationsController implements S
571617
public void post() {}
572618
}
573619

620+
@RestController
621+
@RequestMapping(value = "/controller", consumes = { "application/json" })
622+
interface InterfaceController {
623+
@PostMapping("/postMapping")
624+
void post(@RequestBody(required = false) Foo foo);
625+
}
626+
627+
static class InterfaceControllerImpl implements InterfaceController {
628+
@Override
629+
public void post(Foo foo) {}
630+
}
631+
632+
static class InterfaceControllerImplOverridesRequestBody implements InterfaceController {
633+
@Override
634+
public void post(@RequestBody(required = true) Foo foo) {}
635+
}
574636

575637
@HttpExchange
576638
@Target(ElementType.TYPE)

0 commit comments

Comments
 (0)