diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/HandlerResultHandlerSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/HandlerResultHandlerSupport.java index 530ac2b7d0..8b12145f5f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/HandlerResultHandlerSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/HandlerResultHandlerSupport.java @@ -152,6 +152,10 @@ public abstract class HandlerResultHandlerSupport implements Ordered { private List getProducibleTypes(ServerWebExchange exchange, Supplier> producibleTypesSupplier) { + MediaType contentType = exchange.getResponse().getHeaders().getContentType(); + if (contentType != null && contentType.isConcrete()) { + return Collections.singletonList(contentType); + } Set mediaTypes = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); return (mediaTypes != null ? new ArrayList<>(mediaTypes) : producibleTypesSupplier.get()); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java index 75e4f751cb..230a6f219f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java @@ -111,7 +111,7 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); - MediaType bestMediaType = selectMediaType(exchange, () -> getProducibleMediaTypes(elementType)); + MediaType bestMediaType = selectMediaType(exchange, () -> getMediaTypesFor(elementType)); if (bestMediaType != null) { for (HttpMessageWriter writer : getMessageWriters()) { if (writer.canWrite(elementType, bestMediaType)) { @@ -121,12 +121,12 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa } } else { - if (getProducibleMediaTypes(elementType).isEmpty()) { - return Mono.error(new IllegalStateException("No converter for return value type: " + elementType)); + if (getMediaTypesFor(elementType).isEmpty()) { + return Mono.error(new IllegalStateException("No writer for : " + elementType)); } } - return Mono.error(new NotAcceptableStatusException(getProducibleMediaTypes(elementType))); + return Mono.error(new NotAcceptableStatusException(getMediaTypesFor(elementType))); } private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType genericType) { @@ -141,7 +141,7 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa } } - private List getProducibleMediaTypes(ResolvableType elementType) { + private List getMediaTypesFor(ResolvableType elementType) { return getMessageWriters().stream() .filter(converter -> converter.canWrite(elementType, null)) .flatMap(converter -> converter.getWritableMediaTypes().stream()) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java index 3bc66b13e4..6850f3f35e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -155,6 +155,16 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq assertEquals(expected, performGet("/person-response/mono-response-entity", JSON, Person.class).getBody()); } + @Test // SPR-16172 + public void personResponseBodyWithMonoResponseEntityXml() throws Exception { + + String actual = performGet("/person-response/mono-response-entity-xml", + new HttpHeaders(), String.class).getBody(); + + assertEquals("" + + "Robert", actual); + } + @Test public void personResponseBodyWithList() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); @@ -465,6 +475,12 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq return ResponseEntity.ok(body); } + @GetMapping("/mono-response-entity-xml") + public ResponseEntity> getMonoResponseEntityXml() { + Mono body = Mono.just(new Person("Robert")); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(body); + } + @GetMapping("/list") public List getList() { return asList(new Person("Robert"), new Person("Marie")); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 2755d00b53..9cc950017f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -212,7 +212,10 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe HttpServletRequest request = inputMessage.getServletRequest(); List requestedMediaTypes = getAcceptableMediaTypes(request); - List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); + + MediaType contentType = outputMessage.getHeaders().getContentType(); + List producibleMediaTypes = (contentType != null && contentType.isConcrete() ? + Collections.singletonList(contentType) : getProducibleMediaTypes(request, valueType, declaredType)); if (outputValue != null && producibleMediaTypes.isEmpty()) { throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 53ee982bdf..bd26cd515f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -83,6 +83,8 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MarshallingHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.test.MockHttpServletRequest; @@ -934,6 +936,26 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals(404, response.getStatus()); } + @Test // SPR-16172 + public void httpEntityWithContentType() throws Exception { + initServlet(wac -> { + RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); + List> messageConverters = new ArrayList<>(); + messageConverters.add(new MappingJackson2HttpMessageConverter()); + messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + adapterDef.getPropertyValues().add("messageConverters", messageConverters); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + }, ResponseEntityController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test-entity"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals(200, response.getStatus()); + assertEquals("application/xml", response.getHeader("Content-Type")); + assertEquals("" + + "Foo Bar", response.getContentAsString()); + } + @Test // SPR-6877 public void overlappingMessageConvertersRequestBody() throws Exception { initServlet(wac -> { @@ -3210,7 +3232,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @Controller public static class ResponseEntityController { - @RequestMapping(path = "/foo", method = RequestMethod.POST) + @PostMapping("/foo") public ResponseEntity foo(HttpEntity requestEntity) throws Exception { assertNotNull(requestEntity); assertEquals("MyValue", requestEntity.getHeaders().getFirst("MyRequestHeader")); @@ -3222,12 +3244,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl return ResponseEntity.created(location).header("MyResponseHeader", "MyValue").body(body); } - @RequestMapping(path = "/bar", method = RequestMethod.GET) + @GetMapping("/bar") public ResponseEntity bar() { return ResponseEntity.notFound().header("MyResponseHeader", "MyValue").build(); } - @RequestMapping(path = "/baz", method = RequestMethod.GET) + @GetMapping("/baz") public ResponseEntity baz() { return ResponseEntity.ok().header("MyResponseHeader", "MyValue").body("body"); } @@ -3237,10 +3259,32 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl return ResponseEntity.ok().header("h1", "v1").build(); } - @RequestMapping(path = "/stores", method = RequestMethod.GET) + @GetMapping("/stores") public ResponseEntity getResource() { return ResponseEntity.ok().body("body"); } + + @GetMapping("/test-entity") + public ResponseEntity testEntity() { + TestEntity entity = new TestEntity(); + entity.setName("Foo Bar"); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(entity); + } + } + + @XmlRootElement + static class TestEntity { + + private String name; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Controller