Browse Source

Revert "Infer HTTP 404 from empty Optional/Publisher types"

This reverts commit 7e91733502.
pull/1929/head
Brian Clozel 7 years ago
parent
commit
f2506ca7a1
  1. 5
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
  2. 14
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
  3. 16
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
  4. 167
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
  5. 22
      src/docs/asciidoc/web/webflux.adoc
  6. 17
      src/docs/asciidoc/web/webmvc.adoc

5
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java

@ -55,8 +55,6 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand @@ -55,8 +55,6 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
private static final ResponseEntity<Object> notFound = new ResponseEntity<>(HttpStatus.NOT_FOUND);
/**
* Basic constructor with a default {@link ReactiveAdapterRegistry}.
@ -121,8 +119,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand @@ -121,8 +119,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
if (adapter != null) {
Assert.isTrue(!adapter.isMultiValue(), "Only a single ResponseEntity supported");
returnValueMono = Mono.from(adapter.toPublisher(result.getReturnValue()))
.defaultIfEmpty(notFound);
returnValueMono = Mono.from(adapter.toPublisher(result.getReturnValue()));
bodyParameter = actualParameter.nested().nested();
}
else {

14
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java

@ -350,20 +350,6 @@ public class ResponseEntityResultHandlerTests { @@ -350,20 +350,6 @@ public class ResponseEntityResultHandlerTests {
assertResponseBodyIsEmpty(exchange);
}
@Test // SPR-13281
public void handleEmptyMonoShouldResultInNotFoundStatus() {
MockServerWebExchange exchange = MockServerWebExchange.from(get("/path"));
exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(APPLICATION_JSON));
MethodParameter type = on(TestController.class).resolveReturnType(Mono.class, ResponseEntity.class);
HandlerResult result = new HandlerResult(new TestController(), Mono.empty(), type);
this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5));
assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode());
assertResponseBodyIsEmpty(exchange);
}
private void testHandle(Object returnValue, MethodParameter returnType) {

16
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java

@ -24,7 +24,6 @@ import java.util.Collections; @@ -24,7 +24,6 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -34,7 +33,6 @@ import org.springframework.core.ResolvableType; @@ -34,7 +33,6 @@ 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;
@ -123,9 +121,8 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @@ -123,9 +121,8 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> parameterType = returnType.nestedIfOptional().getNestedParameterType();
return (HttpEntity.class.isAssignableFrom(parameterType) &&
!RequestEntity.class.isAssignableFrom(parameterType));
return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) &&
!RequestEntity.class.isAssignableFrom(returnType.getParameterType()));
}
@Override
@ -153,7 +150,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @@ -153,7 +150,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
@Nullable
private Type getHttpEntityType(MethodParameter parameter) {
Assert.isAssignable(HttpEntity.class, parameter.getNestedParameterType());
Assert.isAssignable(HttpEntity.class, parameter.getParameterType());
Type parameterType = parameter.getGenericParameterType();
if (parameterType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) parameterType;
@ -172,7 +169,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @@ -172,7 +169,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
}
@Override
@SuppressWarnings("unchecked")
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
@ -184,12 +180,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @@ -184,12 +180,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
if (returnType.getParameterType() == Optional.class) {
Optional<HttpEntity<?>> optionalEntity = (Optional<HttpEntity<?>>) returnValue;
returnValue = optionalEntity.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
returnType = returnType.nestedIfOptional();
}
Assert.isInstanceOf(HttpEntity.class, returnValue);
HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;

167
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java

@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation; @@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
@ -27,7 +28,6 @@ import java.time.temporal.ChronoUnit; @@ -27,7 +28,6 @@ import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Optional;
import org.hamcrest.Matchers;
import org.junit.Before;
@ -56,17 +56,14 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -56,17 +56,14 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.ResolvableMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import static java.time.Instant.ofEpochMilli;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static java.time.Instant.*;
import static java.time.format.DateTimeFormatter.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
import static org.springframework.http.MediaType.TEXT_PLAIN;
import static org.springframework.web.method.ResolvableMethod.on;
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
import static org.springframework.http.MediaType.*;
import static org.springframework.web.servlet.HandlerMapping.*;
/**
* Test fixture for {@link HttpEntityMethodProcessor} delegating to a mock
@ -94,6 +91,26 @@ public class HttpEntityMethodProcessorMockTests { @@ -94,6 +91,26 @@ public class HttpEntityMethodProcessorMockTests {
private HttpMessageConverter<Object> resourceRegionMessageConverter;
private MethodParameter paramHttpEntity;
private MethodParameter paramRequestEntity;
private MethodParameter paramResponseEntity;
private MethodParameter paramInt;
private MethodParameter returnTypeResponseEntity;
private MethodParameter returnTypeResponseEntityProduces;
private MethodParameter returnTypeResponseEntityResource;
private MethodParameter returnTypeHttpEntity;
private MethodParameter returnTypeHttpEntitySubclass;
private MethodParameter returnTypeInt;
private ModelAndViewContainer mavContainer;
private MockHttpServletRequest servletRequest;
@ -102,12 +119,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -102,12 +119,6 @@ public class HttpEntityMethodProcessorMockTests {
private ServletWebRequest webRequest;
private MethodParameter returnTypeResponseEntity =
on(TestHandler.class).resolveReturnType(ResponseEntity.class, String.class);
private MethodParameter returnTypeResponseEntityResource =
on(TestHandler.class).resolveReturnType(ResponseEntity.class, Resource.class);
@Before
@SuppressWarnings("unchecked")
@ -128,6 +139,20 @@ public class HttpEntityMethodProcessorMockTests { @@ -128,6 +139,20 @@ public class HttpEntityMethodProcessorMockTests {
processor = new HttpEntityMethodProcessor(Arrays.asList(
stringHttpMessageConverter, resourceMessageConverter, resourceRegionMessageConverter));
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class,
Integer.TYPE, RequestEntity.class);
paramHttpEntity = new MethodParameter(handle1, 0);
paramRequestEntity = new MethodParameter(handle1, 3);
paramResponseEntity = new MethodParameter(handle1, 1);
paramInt = new MethodParameter(handle1, 2);
returnTypeResponseEntity = new MethodParameter(handle1, -1);
returnTypeResponseEntityProduces = new MethodParameter(getClass().getMethod("handle4"), -1);
returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1);
returnTypeHttpEntitySubclass = new MethodParameter(getClass().getMethod("handle2x", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
returnTypeResponseEntityResource = new MethodParameter(getClass().getMethod("handle5"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest("GET", "/foo");
servletResponse = new MockHttpServletResponse();
@ -137,39 +162,25 @@ public class HttpEntityMethodProcessorMockTests { @@ -137,39 +162,25 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void supportsParameter() {
ResolvableMethod method = on(TestHandler.class).named("entityParams").build();
assertTrue("HttpEntity parameter not supported",
processor.supportsParameter(method.arg(HttpEntity.class, String.class)));
assertTrue("RequestEntity parameter not supported",
processor.supportsParameter(method.arg(RequestEntity.class, String.class)));
assertFalse("ResponseEntity parameter supported",
processor.supportsParameter(method.arg(ResponseEntity.class, String.class)));
assertFalse("non-entity parameter supported",
processor.supportsParameter(method.arg(Integer.class)));
assertTrue("HttpEntity parameter not supported", processor.supportsParameter(paramHttpEntity));
assertTrue("RequestEntity parameter not supported", processor.supportsParameter(paramRequestEntity));
assertFalse("ResponseEntity parameter supported", processor.supportsParameter(paramResponseEntity));
assertFalse("non-entity parameter supported", processor.supportsParameter(paramInt));
}
@Test
public void supportsReturnType() {
assertTrue("ResponseEntity return type not supported",
processor.supportsReturnType(on(TestHandler.class).resolveReturnType(ResponseEntity.class, String.class)));
assertTrue("HttpEntity return type not supported",
processor.supportsReturnType(on(TestHandler.class).resolveReturnType(HttpEntity.class)));
assertTrue("Custom HttpEntity subclass not supported",
processor.supportsReturnType(on(TestHandler.class).resolveReturnType(CustomHttpEntity.class)));
assertTrue("Optional ResponseEntity not supported",
processor.supportsReturnType(on(TestHandler.class).resolveReturnType(Optional.class, ResponseEntity.class)));
assertFalse("RequestEntity return type supported",
processor.supportsReturnType(on(TestHandler.class)
.named("entityParams").build().arg(RequestEntity.class, String.class)));
assertFalse("non-ResponseBody return type supported",
processor.supportsReturnType(on(TestHandler.class).resolveReturnType(Integer.class)));
assertTrue("ResponseEntity return type not supported", processor.supportsReturnType(returnTypeResponseEntity));
assertTrue("HttpEntity return type not supported", processor.supportsReturnType(returnTypeHttpEntity));
assertTrue("Custom HttpEntity subclass not supported", processor.supportsReturnType(returnTypeHttpEntitySubclass));
assertFalse("RequestEntity parameter supported",
processor.supportsReturnType(paramRequestEntity));
assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt));
}
@Test
public void shouldResolveHttpEntityArgument() throws Exception {
String body = "Foo";
MethodParameter paramHttpEntity = on(TestHandler.class).named("entityParams")
.build().arg(HttpEntity.class, String.class);
MediaType contentType = TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
@ -188,8 +199,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -188,8 +199,6 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void shouldResolveRequestEntityArgument() throws Exception {
String body = "Foo";
MethodParameter paramRequestEntity = on(TestHandler.class).named("entityParams")
.build().arg(RequestEntity.class, String.class);
MediaType contentType = TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
@ -216,8 +225,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -216,8 +225,6 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void shouldFailResolvingWhenConverterCannotRead() throws Exception {
MethodParameter paramHttpEntity = on(TestHandler.class).named("entityParams")
.build().arg(HttpEntity.class, String.class);
MediaType contentType = TEXT_PLAIN;
servletRequest.setMethod("POST");
servletRequest.addHeader("Content-Type", contentType.toString());
@ -231,8 +238,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -231,8 +238,6 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void shouldFailResolvingWhenContentTypeNotSupported() throws Exception {
MethodParameter paramHttpEntity = on(TestHandler.class).named("entityParams")
.build().arg(HttpEntity.class, String.class);
servletRequest.setMethod("POST");
servletRequest.setContent("some content".getBytes(StandardCharsets.UTF_8));
this.thrown.expect(HttpMediaTypeNotSupportedException.class);
@ -255,14 +260,13 @@ public class HttpEntityMethodProcessorMockTests { @@ -255,14 +260,13 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void shouldHandleReturnValueWithProducibleMediaType() throws Exception {
MethodParameter returnType = on(TestHandler.class).resolveReturnType(ResponseEntity.class, CharSequence.class);
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<>(body, HttpStatus.OK);
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
given(stringHttpMessageConverter.canWrite(String.class, MediaType.TEXT_HTML)).willReturn(true);
processor.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(stringHttpMessageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
@ -307,7 +311,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -307,7 +311,6 @@ public class HttpEntityMethodProcessorMockTests {
@Test
public void shouldFailHandlingWhenConverterCannotWrite() throws Exception {
MethodParameter returnType = on(TestHandler.class).resolveReturnType(ResponseEntity.class, CharSequence.class);
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<>(body, HttpStatus.OK);
MediaType accepted = TEXT_PLAIN;
@ -319,7 +322,7 @@ public class HttpEntityMethodProcessorMockTests { @@ -319,7 +322,7 @@ public class HttpEntityMethodProcessorMockTests {
given(stringHttpMessageConverter.canWrite(String.class, accepted)).willReturn(false);
this.thrown.expect(HttpMediaTypeNotAcceptableException.class);
processor.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
}
@Test // SPR-9142
@ -358,16 +361,6 @@ public class HttpEntityMethodProcessorMockTests { @@ -358,16 +361,6 @@ public class HttpEntityMethodProcessorMockTests {
assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0));
}
@Test // SPR-13281
public void shouldReturnNotFoundWhenEmptyOptional() throws Exception {
MethodParameter returnType = on(TestHandler.class).resolveReturnType(Optional.class, ResponseEntity.class);
Optional<ResponseEntity> returnValue = Optional.empty();
processor.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
assertEquals(HttpStatus.NOT_FOUND.value(), servletResponse.getStatus());
}
@Test
public void shouldHandleLastModifiedWithHttp304() throws Exception {
long currentTime = new Date().getTime();
@ -704,46 +697,38 @@ public class HttpEntityMethodProcessorMockTests { @@ -704,46 +697,38 @@ public class HttpEntityMethodProcessorMockTests {
}
}
private static class TestHandler {
@SuppressWarnings("unused")
public ResponseEntity<String> entityParams(HttpEntity<String> httpEntity, ResponseEntity<String> entity,
Integer i, RequestEntity<String> requestEntity) {
return entity;
}
@SuppressWarnings("unused")
public HttpEntity<?> httpEntity(HttpEntity<?> entity) {
return entity;
}
@SuppressWarnings("unused")
public ResponseEntity<String> handle1(HttpEntity<String> httpEntity, ResponseEntity<String> entity,
int i, RequestEntity<String> requestEntity) {
@SuppressWarnings("unused")
public CustomHttpEntity customHttpEntity(HttpEntity<?> entity) {
return new CustomHttpEntity();
}
return entity;
}
@SuppressWarnings("unused")
public Optional<ResponseEntity> handleOptionalEntity() {
return null;
}
@SuppressWarnings("unused")
public HttpEntity<?> handle2(HttpEntity<?> entity) {
return entity;
}
@SuppressWarnings("unused")
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
public ResponseEntity<CharSequence> produces() {
return null;
}
@SuppressWarnings("unused")
public CustomHttpEntity handle2x(HttpEntity<?> entity) {
return new CustomHttpEntity();
}
@SuppressWarnings("unused")
public ResponseEntity<Resource> resourceResponseEntity() {
return null;
}
@SuppressWarnings("unused")
public int handle3() {
return 42;
}
@SuppressWarnings("unused")
public Integer integer() {
return 42;
}
@SuppressWarnings("unused")
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
public ResponseEntity<String> handle4() {
return null;
}
@SuppressWarnings("unused")
public ResponseEntity<Resource> handle5() {
return null;
}
@SuppressWarnings("unused")

22
src/docs/asciidoc/web/webflux.adoc

@ -1749,10 +1749,14 @@ controller. The following code snippet shows the usage: @@ -1749,10 +1749,14 @@ controller. The following code snippet shows the usage:
[TIP]
====
<<<<<<< HEAD
Unlike the Servlet API "request parameter" concept that conflates query parameters, form
=======
Unlike the Servlet API "request paramater" concept that conflate query parameters, form
>>>>>>> parent of 7e91733502... Infer HTTP 404 from empty Optional/Publisher types
data, and multiparts into one, in WebFlux each is accessed individually through the
`ServerWebExchange`. While `@RequestParam` binds to query parameters only, you can
use data binding to apply query parameters, form data, and multiparts to a
use data binding to apply query paramerters, form data, and multiparts to a
<<webflux-ann-modelattrib-method-args,command object>>.
====
@ -2275,22 +2279,6 @@ on a container object that specifies request headers and body. Below is an examp @@ -2275,22 +2279,6 @@ on a container object that specifies request headers and body. Below is an examp
}
----
As is the case for many return values, `ResponseEntity` can be wrapped by a reactive type
such as `Mono`. In case of a `Mono<ResponseEntity>` return type, an empty `Mono` will be
automatically converted to a `ResponseEntity` with an empty body and an HTTP 404 status,
so you don't need to chain your `Mono` with a `switchIfEmpty` / `defaultIfEmpty` for
simple HTTP 404 responses. Here's an example of this:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
@GetMapping("/user")
public Mono<ResponseEntity<User>> fetchUser() {
Mono<User> user = //...
return user.map(ResponseEntity::ok);
}
----
[[webflux-ann-jackson]]
==== Jackson JSON

17
src/docs/asciidoc/web/webmvc.adoc

@ -1816,7 +1816,7 @@ supported for all return values, see below for more details. @@ -1816,7 +1816,7 @@ supported for all return values, see below for more details.
| The return value is converted through ``HttpMessageConverter``s and written to the
response. See <<mvc-ann-responsebody>>.
| `HttpEntity<B>`, `ResponseEntity<B>`, `Optional<HttpEntity<B>>`, `Optional<ResponseEntity<B>>`
| `HttpEntity<B>`, `ResponseEntity<B>`
| The return value specifies the full response including HTTP headers and body be converted
through ``HttpMessageConverter``s and written to the response.
See <<mvc-ann-responseentity>>.
@ -2660,21 +2660,6 @@ on a container object that specifies request headers and body. Below is an examp @@ -2660,21 +2660,6 @@ on a container object that specifies request headers and body. Below is an examp
}
----
In case of a `java.util.Optional<ResponseEntity>` return type, the `Optional.empty()`
case will be automatically converted to a `ResponseEntity` with an empty body and an HTTP
404 status, so you don't need to chain your optional with an `orElse` / `orElseGet` for
simple HTTP 404 responses. Here's an example of this:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
@GetMapping("/user")
public Optional<ResponseEntity<User>> fetchUser() {
Optional<User> user = //...
return user.map(ResponseEntity::ok);
}
----
[[mvc-ann-jackson]]
==== Jackson JSON

Loading…
Cancel
Save