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:
Rossen Stoyanchev
2015-03-29 21:24:30 -04:00
parent 0556ed4f16
commit 36ed4df59d
9 changed files with 256 additions and 107 deletions

View File

@@ -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;
}
}
}

View File

@@ -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());

View File

@@ -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