Empty body checks in ConsumesRequestCondition

Normally consumes matches the "Content-Type" header but what should be done if
there is no content? This commit adds checks for method parameters with
@RequestBody(required=false) and if "false" then also match requests with no content.

Closes gh-22010
This commit is contained in:
Rossen Stoyanchev
2019-05-07 22:00:34 -04:00
parent cdf51c3d51
commit 45147c23c1
8 changed files with 239 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
@@ -49,8 +50,11 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
private static final ConsumesRequestCondition EMPTY_CONDITION = new ConsumesRequestCondition();
private final List<ConsumeMediaTypeExpression> expressions;
private boolean bodyRequired = true;
/**
* Creates a new instance from 0 or more "consumes" expressions.
@@ -141,6 +145,29 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
return " || ";
}
/**
* Whether this condition should expect requests to have a body.
* <p>By default this is set to {@code true} in which case it is assumed a
* request body is required and this condition matches to the "Content-Type"
* header or falls back on "Content-Type: application/octet-stream".
* <p>If set to {@code false}, and the request does not have a body, then this
* condition matches automatically, i.e. without checking expressions.
* @param bodyRequired whether requests are expected to have a body
* @since 5.2
*/
public void setBodyRequired(boolean bodyRequired) {
this.bodyRequired = bodyRequired;
}
/**
* Return the setting for {@link #setBodyRequired(boolean)}.
* @since 5.2
*/
public boolean isBodyRequired() {
return this.bodyRequired;
}
/**
* Returns the "other" instance if it has any expressions; returns "this"
* instance otherwise. Practically that means a method-level "consumes"
@@ -170,14 +197,17 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
if (isEmpty()) {
return this;
}
if (!hasBody(request) && !this.bodyRequired) {
return EMPTY_CONDITION;
}
// Common media types are cached at the level of MimeTypeUtils
MediaType contentType;
try {
contentType = (StringUtils.hasLength(request.getContentType()) ?
contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM);
MediaType.APPLICATION_OCTET_STREAM;
}
catch (InvalidMediaTypeException ex) {
return null;
@@ -187,6 +217,13 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
return !CollectionUtils.isEmpty(result) ? new ConsumesRequestCondition(result) : null;
}
private boolean hasBody(HttpServletRequest request) {
String contentLength = request.getHeader(HttpHeaders.CONTENT_LENGTH);
String transferEncoding = request.getHeader(HttpHeaders.TRANSFER_ENCODING);
return StringUtils.hasText(transferEncoding) ||
(StringUtils.hasText(contentLength) && !contentLength.trim().equals("0"));
}
@Nullable
private List<ConsumeMediaTypeExpression> getMatchingExpressions(MediaType contentType) {
List<ConsumeMediaTypeExpression> result = null;

View File

@@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -28,6 +29,8 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
@@ -35,6 +38,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
@@ -43,6 +47,7 @@ import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
@@ -332,6 +337,31 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
}
@Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method);
updateConsumesCondition(mapping, method);
}
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
updateConsumesCondition(mapping, method);
}
private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) {
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
}
}
}
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) {
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();