Invoke handleEmptyBody if there is no content-type
Closes gh-30522
This commit is contained in:
parent
d8ed7c7906
commit
d8441fc80c
|
|
@ -23,6 +23,7 @@ import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
@ -38,6 +39,7 @@ import org.springframework.core.log.LogFormatUtils;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpInputMessage;
|
import org.springframework.http.HttpInputMessage;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpOutputMessage;
|
||||||
import org.springframework.http.HttpRequest;
|
import org.springframework.http.HttpRequest;
|
||||||
import org.springframework.http.InvalidMediaTypeException;
|
import org.springframework.http.InvalidMediaTypeException;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
@ -170,7 +172,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||||
EmptyBodyCheckingHttpInputMessage message = null;
|
EmptyBodyCheckingHttpInputMessage message = null;
|
||||||
try {
|
try {
|
||||||
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
|
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
|
||||||
|
|
||||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||||
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
|
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
|
||||||
GenericHttpMessageConverter<?> genericConverter =
|
GenericHttpMessageConverter<?> genericConverter =
|
||||||
|
|
@ -190,6 +191,10 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (body == NO_VALUE && noContentType && !message.hasBody()) {
|
||||||
|
body = getAdvice().handleEmptyBody(
|
||||||
|
null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
|
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
|
||||||
|
|
@ -201,8 +206,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body == NO_VALUE) {
|
if (body == NO_VALUE) {
|
||||||
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
|
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) {
|
||||||
(noContentType && !message.hasBody())) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
throw new HttpMediaTypeNotSupportedException(contentType,
|
throw new HttpMediaTypeNotSupportedException(contentType,
|
||||||
|
|
@ -354,4 +358,38 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder HttpMessageConverter type to pass to RequestBodyAdvice if there
|
||||||
|
* is no content-type and no content. In that case, we may not find a converter,
|
||||||
|
* but RequestBodyAdvice have a chance to provide it via handleEmptyBody.
|
||||||
|
*/
|
||||||
|
private static class NoContentTypeHttpMessageConverter implements HttpMessageConverter<String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MediaType> getSupportedMediaTypes() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String read(Class<? extends String> clazz, HttpInputMessage inputMessage) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String s, @Nullable MediaType contentType, HttpOutputMessage outputMessage) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.groovy.util.Maps;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -45,6 +46,8 @@ import org.springframework.lang.Nullable;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
|
@ -109,7 +112,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void cacheControlWithoutSessionAttributes() throws Exception {
|
public void cacheControlWithoutSessionAttributes() throws Exception {
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
|
||||||
this.handlerAdapter.setCacheSeconds(100);
|
this.handlerAdapter.setCacheSeconds(100);
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
|
|
||||||
|
|
@ -197,7 +200,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
|
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
|
||||||
this.webAppContext.refresh();
|
this.webAppContext.refresh();
|
||||||
|
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||||
|
|
||||||
|
|
@ -210,7 +213,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
this.webAppContext.registerPrototype("maa", ModelAttributeAdvice.class);
|
this.webAppContext.registerPrototype("maa", ModelAttributeAdvice.class);
|
||||||
this.webAppContext.refresh();
|
this.webAppContext.refresh();
|
||||||
|
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
Map<String, Object> model1 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
|
Map<String, Object> model1 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
|
||||||
Map<String, Object> model2 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
|
Map<String, Object> model2 = this.handlerAdapter.handle(this.request, this.response, handlerMethod).getModel();
|
||||||
|
|
@ -226,7 +229,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
this.webAppContext.setParent(parent);
|
this.webAppContext.setParent(parent);
|
||||||
this.webAppContext.refresh();
|
this.webAppContext.refresh();
|
||||||
|
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||||
|
|
||||||
|
|
@ -240,7 +243,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
this.webAppContext.registerSingleton("manupa", ModelAttributeNotUsedPackageAdvice.class);
|
this.webAppContext.registerSingleton("manupa", ModelAttributeNotUsedPackageAdvice.class);
|
||||||
this.webAppContext.refresh();
|
this.webAppContext.refresh();
|
||||||
|
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handle");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handle");
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
ModelAndView mav = this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||||
|
|
||||||
|
|
@ -249,9 +252,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
assertThat(mav.getModel().get("attr3")).isNull();
|
assertThat(mav.getModel().get("attr3")).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPR-10859
|
@Test // gh-15486
|
||||||
|
|
||||||
@Test
|
|
||||||
public void responseBodyAdvice() throws Exception {
|
public void responseBodyAdvice() throws Exception {
|
||||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
||||||
converters.add(new MappingJackson2HttpMessageConverter());
|
converters.add(new MappingJackson2HttpMessageConverter());
|
||||||
|
|
@ -263,7 +264,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
this.request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
|
this.request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE);
|
||||||
this.request.setParameter("c", "callback");
|
this.request.setParameter("c", "callback");
|
||||||
|
|
||||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleBadRequest");
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handleBadRequest");
|
||||||
this.handlerAdapter.afterPropertiesSet();
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||||
|
|
||||||
|
|
@ -271,6 +272,19 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}");
|
assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test // gh-30522
|
||||||
|
public void responseBodyAdviceWithEmptyBody() throws Exception {
|
||||||
|
this.webAppContext.registerBean("rba", EmptyBodyAdvice.class);
|
||||||
|
this.webAppContext.refresh();
|
||||||
|
|
||||||
|
HandlerMethod handlerMethod = handlerMethod(new TestController(), "handleBody", Map.class);
|
||||||
|
this.handlerAdapter.afterPropertiesSet();
|
||||||
|
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||||
|
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(200);
|
||||||
|
assertThat(this.response.getContentAsString()).isEqualTo("Body: {foo=bar}");
|
||||||
|
}
|
||||||
|
|
||||||
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
|
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
|
||||||
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
|
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
|
||||||
return new InvocableHandlerMethod(handler, method);
|
return new InvocableHandlerMethod(handler, method);
|
||||||
|
|
@ -284,7 +298,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static class SimpleController {
|
private static class TestController {
|
||||||
|
|
||||||
@ModelAttribute
|
@ModelAttribute
|
||||||
public void addAttributes(Model model) {
|
public void addAttributes(Model model) {
|
||||||
|
|
@ -296,14 +310,17 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
|
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
|
||||||
return new ResponseEntity<>(Collections.singletonMap(
|
return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK);
|
||||||
"foo", "bar"), HttpStatus.OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> handleBadRequest() {
|
public ResponseEntity<String> handleBadRequest() {
|
||||||
return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
public String handleBody(@Nullable @RequestBody Map<String, String> body) {
|
||||||
|
return "Body: " + body;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -360,6 +377,7 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class additionally implements {@link RequestBodyAdvice} solely for the purpose
|
* This class additionally implements {@link RequestBodyAdvice} solely for the purpose
|
||||||
* of verifying that controller advice implementing both {@link ResponseBodyAdvice}
|
* of verifying that controller advice implementing both {@link ResponseBodyAdvice}
|
||||||
|
|
@ -368,7 +386,8 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
* @see <a href="https://github.com/spring-projects/spring-framework/pull/22638">gh-22638</a>
|
* @see <a href="https://github.com/spring-projects/spring-framework/pull/22638">gh-22638</a>
|
||||||
*/
|
*/
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
private static class ResponseCodeSuppressingAdvice extends AbstractMappingJacksonResponseBodyAdvice implements RequestBodyAdvice {
|
private static class ResponseCodeSuppressingAdvice
|
||||||
|
extends AbstractMappingJacksonResponseBodyAdvice implements RequestBodyAdvice {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
|
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
|
||||||
|
|
@ -405,12 +424,42 @@ public class RequestMappingHandlerAdapterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
|
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
|
||||||
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
|
||||||
return "default value for empty body";
|
return "default value for empty body";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
private static class EmptyBodyAdvice implements RequestBodyAdvice {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(MethodParameter param, Type targetType, Class<? extends HttpMessageConverter<?>> type) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpInputMessage beforeBodyRead(HttpInputMessage message, MethodParameter param,
|
||||||
|
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object afterBodyRead(Object body, HttpInputMessage message, MethodParameter param,
|
||||||
|
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object handleEmptyBody(Object body, HttpInputMessage message, MethodParameter param,
|
||||||
|
Type targetType, Class<? extends HttpMessageConverter<?>> type) {
|
||||||
|
|
||||||
|
return Maps.of("foo", "bar");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue