Description
Francisco Lozano opened SPR-13496 and commented
A new etag-related logic has been introduced that breaks backward compatibility.
In HttpEntityMethodProcessor, the line:
|| clientETag.equals("*"))) {
maybe acceptable for GET, but definitely not for PUT/POST/PATCH verbs, because if I do:
PUT /some/resource
If-None-Match: *
the intention is to create the resource only if it doesn't exist previously. Forcing 304 not modified makes absolutely no sense here, because I want to return 201 created (which I set in my ResponseEntity<MyResponseObject>).
I haven't find any obvious way to disable this magic etag handling, that's why I'm marking this issue as blocker (feel free to downgrade if a workaround is available).
I think all this logic may make sense in some applications, but it can introduce heavy incompatibilities in existing REST APIs.
I wish I had found this when in RC - but I couldn't because of other issues of my components with 4.2 that only now I have been able to fix.
Full changes on this class:
git diff v4.1.7.RELEASE..v4.2.1.RELEASE -- spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
index 26d06ef..22ebdbc 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
@@ -19,18 +19,22 @@ 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.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -48,31 +52,59 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Brian Clozel
* @since 3.1
*/
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
- super(messageConverters);
+
+ /**
+ * Basic constructor with converters only. Suitable for resolving
+ * {@code HttpEntity}. For handling {@code ResponseEntity} consider also
+ * providing a {@code ContentNegotiationManager}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) {
+ super(converters);
+ }
+
+ /**
+ * Basic constructor with converters and {@code ContentNegotiationManager}.
+ * Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity}
+ * without {@code Request~} or {@code ResponseBodyAdvice}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager) {
+
+ super(converters, manager);
}
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager) {
+ /**
+ * Complete constructor for resolving {@code HttpEntity} method arguments.
+ * For handling {@code ResponseEntity} consider also providing a
+ * {@code ContentNegotiationManager}.
+ * @since 4.2
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ List<Object> requestResponseBodyAdvice) {
- super(messageConverters, contentNegotiationManager);
+ super(converters, null, requestResponseBodyAdvice);
}
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) {
+ /**
+ * Complete constructor for resolving {@code HttpEntity} and handling
+ * {@code ResponseEntity}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
- super(messageConverters, contentNegotiationManager, responseBodyAdvice);
+ super(converters, manager, requestResponseBodyAdvice);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
- return (HttpEntity.class.equals(parameter.getParameterType()) ||
- RequestEntity.class.equals(parameter.getParameterType()));
+ return (HttpEntity.class == parameter.getParameterType() ||
+ RequestEntity.class == parameter.getParameterType());
}
@Override
@@ -90,9 +122,10 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Type paramType = getHttpEntityType(parameter);
Object body = readWithMessageConverters(webRequest, parameter, paramType);
- if (RequestEntity.class.equals(parameter.getParameterType())) {
- return new RequestEntity<Object>(body, inputMessage.getHeaders(),
- inputMessage.getMethod(), inputMessage.getURI());
+ if (RequestEntity.class == parameter.getParameterType()) {
+ 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());
@@ -131,22 +164,74 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Assert.isInstanceOf(HttpEntity.class, returnValue);
HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;
- if (responseEntity instanceof ResponseEntity) {
- outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode());
- }
HttpHeaders entityHeaders = responseEntity.getHeaders();
if (!entityHeaders.isEmpty()) {
outputMessage.getHeaders().putAll(entityHeaders);
}
-
Object body = responseEntity.getBody();
+ if (responseEntity instanceof ResponseEntity) {
+ outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode());
+ if (isResourceNotModified(inputMessage, outputMessage)) {
+ outputMessage.setStatusCode(HttpStatus.NOT_MODIFIED);
+ // Ensure headers are flushed, no body should be written.
+ outputMessage.flush();
+ // Skip call to converters, as they may update the body.
+ return;
+ }
+ }
// Try even with null body. ResponseBodyAdvice could get involved.
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
// Ensure headers are flushed even if no body was written.
- outputMessage.getBody();
+ outputMessage.flush();
+ }
+
+ private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
+
+ List<String> ifNoneMatch = inputMessage.getHeaders().getIfNoneMatch();
+ long ifModifiedSince = inputMessage.getHeaders().getIfModifiedSince();
+ String eTag = addEtagPadding(outputMessage.getHeaders().getETag());
+ long lastModified = outputMessage.getHeaders().getLastModified();
+ boolean notModified = false;
+
+ if (lastModified != -1 && StringUtils.hasLength(eTag)) {
+ notModified = isETagNotModified(ifNoneMatch, eTag) && isTimeStampNotModified(ifModifiedSince, lastModified);
+ }
+ else if (lastModified != -1) {
+ notModified = isTimeStampNotModified(ifModifiedSince, lastModified);
+ }
+ else if (StringUtils.hasLength(eTag)) {
+ notModified = isETagNotModified(ifNoneMatch, eTag);
+ }
+ return notModified;
+ }
+
+ private boolean isETagNotModified(List<String> ifNoneMatch, String etag) {
+ if (StringUtils.hasLength(etag)) {
+ for (String clientETag : ifNoneMatch) {
+ // compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
+ if (StringUtils.hasLength(clientETag) &&
+ (clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", ""))
+ || clientETag.equals("*"))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isTimeStampNotModified(long ifModifiedSince, long lastModifiedTimestamp) {
+ return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
+ }
+
+ private String addEtagPadding(String etag) {
+ if (StringUtils.hasLength(etag) &&
+ (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) ) {
+ etag = "\"" + etag + "\"";
+ }
+ return etag;
}
@Override
Affects: 4.2 GA, 4.2.1
Issue Links:
- ServletWebRequest.checkNotModified(…) writes Last-Modified header in invalid format [SPR-13090] #17681 ServletWebRequest.checkNotModified(…) writes Last-Modified header in invalid format
- Support for conditional PUT in Web MVC (using If-Unmodified-Since header) [SPR-13863] #18436 Support for conditional PUT in Web MVC (using If-Unmodified-Since header)
- Improve and/or provide control over ETag/If-None-Match logic in HttpEntityMethodProcessor [SPR-13626] #18204 Improve and/or provide control over ETag/If-None-Match logic in HttpEntityMethodProcessor
Referenced from: commits 583a48a
1 votes, 5 watchers