Improve empty request body handling
The check for an empty request body InputStream is now in the base class AbstractMessageConverterMethodArgumentResolver shared for all arguments that involve reading with an HttpMessageConverter -- @RequestBody, @RequestPart, and HttpEntity. When an empty body is detected any configured RequestBodyAdvice is given a chance to select a default value or leave it as null. Issue: SPR-12778, SPR-12860, SPR-12861
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PushbackInputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
@@ -33,6 +35,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.InvalidMediaTypeException;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -57,6 +60,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
*/
|
||||
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private static final Object NO_VALUE = new Object();
|
||||
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
protected final List<HttpMessageConverter<?>> messageConverters;
|
||||
@@ -135,9 +141,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
* @param <T> the expected type of the argument value to be created
|
||||
* @param inputMessage the HTTP input message representing the current request
|
||||
* @param param the method parameter descriptor (may be {@code null})
|
||||
* @param targetType the type of object to create, not necessarily the same as
|
||||
* the method parameter type (e.g. for {@code HttpEntity<String>} method
|
||||
* parameter the target type is String)
|
||||
* @param targetType the target type, not necessarily the same as the method
|
||||
* parameter type, e.g. for {@code HttpEntity<String>}.
|
||||
* @return the created method argument value
|
||||
* @throws IOException if the reading from the request fails
|
||||
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
|
||||
@@ -165,6 +170,9 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
targetClass = (Class<T>) resolvableType.resolve();
|
||||
}
|
||||
|
||||
inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);
|
||||
Object body = NO_VALUE;
|
||||
|
||||
for (HttpMessageConverter<?> converter : this.messageConverters) {
|
||||
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
|
||||
if (converter instanceof GenericHttpMessageConverter) {
|
||||
@@ -173,9 +181,16 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
|
||||
}
|
||||
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
|
||||
T body = (T) genericConverter.read(targetType, contextClass, inputMessage);
|
||||
return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
|
||||
if (inputMessage.getBody() != null) {
|
||||
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
|
||||
body = genericConverter.read(targetType, contextClass, inputMessage);
|
||||
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
|
||||
}
|
||||
else {
|
||||
body = null;
|
||||
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (targetClass != null) {
|
||||
@@ -183,14 +198,25 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
|
||||
}
|
||||
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
|
||||
T body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
|
||||
return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
|
||||
if (inputMessage.getBody() != null) {
|
||||
inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
|
||||
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
|
||||
body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
|
||||
}
|
||||
else {
|
||||
body = null;
|
||||
body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
|
||||
if (body == NO_VALUE) {
|
||||
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,4 +266,47 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
|
||||
return !hasBindingResult;
|
||||
}
|
||||
|
||||
|
||||
private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final InputStream body;
|
||||
|
||||
|
||||
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
|
||||
this.headers = inputMessage.getHeaders();
|
||||
InputStream inputStream = inputMessage.getBody();
|
||||
if (inputStream == null) {
|
||||
this.body = null;
|
||||
}
|
||||
else if (inputStream.markSupported()) {
|
||||
inputStream.mark(1);
|
||||
this.body = (inputStream.read() != -1 ? inputStream : null);
|
||||
inputStream.reset();
|
||||
}
|
||||
else {
|
||||
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
|
||||
int b = pushbackInputStream.read();
|
||||
if (b == -1) {
|
||||
this.body = null;
|
||||
}
|
||||
else {
|
||||
this.body = pushbackInputStream;
|
||||
pushbackInputStream.unread(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() throws IOException {
|
||||
return this.body;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,12 +19,14 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -120,8 +122,9 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
||||
|
||||
Object body = readWithMessageConverters(webRequest, parameter, paramType);
|
||||
if (RequestEntity.class.equals(parameter.getParameterType())) {
|
||||
return new RequestEntity<Object>(body, inputMessage.getHeaders(),
|
||||
inputMessage.getMethod(), inputMessage.getURI());
|
||||
URI url = inputMessage.getURI();
|
||||
HttpMethod httpMethod = inputMessage.getMethod();
|
||||
return new RequestEntity<Object>(body, inputMessage.getHeaders(), httpMethod, url);
|
||||
}
|
||||
else {
|
||||
return new HttpEntity<Object>(body, inputMessage.getHeaders());
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PushbackInputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
@@ -146,43 +144,14 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
||||
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);
|
||||
|
||||
InputStream inputStream = inputMessage.getBody();
|
||||
if (inputStream == null) {
|
||||
return handleEmptyBody(methodParam);
|
||||
}
|
||||
else if (inputStream.markSupported()) {
|
||||
inputStream.mark(1);
|
||||
if (inputStream.read() == -1) {
|
||||
return handleEmptyBody(methodParam);
|
||||
Object arg = readWithMessageConverters(inputMessage, methodParam, paramType);
|
||||
if (arg == null) {
|
||||
if (methodParam.getParameterAnnotation(RequestBody.class).required()) {
|
||||
throw new HttpMessageNotReadableException("Required request body is missing: " + methodParam);
|
||||
}
|
||||
inputStream.reset();
|
||||
}
|
||||
else {
|
||||
final PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
|
||||
int b = pushbackInputStream.read();
|
||||
if (b == -1) {
|
||||
return handleEmptyBody(methodParam);
|
||||
}
|
||||
else {
|
||||
pushbackInputStream.unread(b);
|
||||
}
|
||||
inputMessage = new ServletServerHttpRequest(servletRequest) {
|
||||
@Override
|
||||
public InputStream getBody() {
|
||||
// Form POST should not get here
|
||||
return pushbackInputStream;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return super.readWithMessageConverters(inputMessage, methodParam, paramType);
|
||||
}
|
||||
|
||||
private Object handleEmptyBody(MethodParameter param) {
|
||||
if (param.getParameterAnnotation(RequestBody.class).required()) {
|
||||
throw new HttpMessageNotReadableException("Required request body content is missing: " + param);
|
||||
}
|
||||
return null;
|
||||
return arg;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user