From ed5cc27f7bf09b395af2471f62cc068afe27383f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 13 Jul 2017 11:11:34 +0200 Subject: [PATCH] Support empty body without content type in WebFlux Issue: SPR-15758 --- ...AbstractMessageReaderArgumentResolver.java | 30 ++++++++++++++++--- .../MessageReaderArgumentResolverTests.java | 4 ++- .../RequestBodyArgumentResolverTests.java | 10 +++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java index e4d4bc35669..34d061e98c5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java @@ -18,8 +18,10 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import reactor.core.publisher.Flux; @@ -32,6 +34,8 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -62,6 +66,10 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException; */ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMethodArgumentResolverSupport { + private static final Set SUPPORTED_METHODS = + EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); + + private final List> messageReaders; private final List supportedMediaTypes; @@ -111,10 +119,9 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); - MediaType mediaType = request.getHeaders().getContentType(); - if (mediaType == null) { - mediaType = MediaType.APPLICATION_OCTET_STREAM; - } + + MediaType contentType = request.getHeaders().getContentType(); + MediaType mediaType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); for (HttpMessageReader reader : getMessageReaders()) { if (reader.canRead(elementType, mediaType)) { @@ -133,6 +140,7 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho return Mono.just(adapter.fromPublisher(flux)); } else { + // Single-value (with or without reactive type wrapper) Mono mono = reader.readMono(bodyType, elementType, request, response, readHints); mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParameter, ex))); if (isBodyRequired || (adapter != null && !adapter.supportsEmpty())) { @@ -153,6 +161,20 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho } } + // No compatible reader but body may be empty.. + + HttpMethod method = request.getMethod(); + if (contentType == null && method != null && SUPPORTED_METHODS.contains(method)) { + Flux body = request.getBody().doOnNext(o -> { + // Body not empty, back to 415.. + throw new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes); + }); + if (isBodyRequired || (adapter != null && !adapter.supportsEmpty())) { + body = body.switchIfEmpty(Mono.error(handleMissingBody(bodyParameter))); + } + return (adapter != null ? Mono.just(adapter.fromPublisher(body)) : Mono.from(body)); + } + return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java index ce283e23c78..5faf70caa52 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java @@ -83,14 +83,16 @@ public class MessageReaderArgumentResolverTests { } + @SuppressWarnings("unchecked") @Test public void missingContentType() throws Exception { ServerWebExchange exchange = post("/path").body("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}").toExchange(); ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); MethodParameter param = this.testMethod.arg(type); Mono result = this.resolver.readBody(param, true, this.bindingContext, exchange); + Mono value = (Mono) result.block(Duration.ofSeconds(1)); - StepVerifier.create(result).expectError(UnsupportedMediaTypeStatusException.class).verify(); + StepVerifier.create(value).expectError(UnsupportedMediaTypeStatusException.class).verify(); } // More extensive "empty body" tests in RequestBody- and HttpEntityArgumentResolverTests diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index f9e993bac4b..a3dfa384d71 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import io.reactivex.Maybe; @@ -106,6 +107,14 @@ public class RequestBodyArgumentResolverTests { assertNull(body); } + @Test // SPR-15758 + public void emptyBodyWithoutContentType() throws Exception { + MethodParameter param = this.testMethod.annot(requestBody().notRequired()).arg(Map.class); + String body = resolveValueWithEmptyBody(param); + + assertNull(body); + } + @Test @SuppressWarnings("unchecked") public void emptyBodyWithMono() throws Exception { @@ -262,6 +271,7 @@ public class RequestBodyArgumentResolverTests { @RequestBody(required = false) Observable obsNotRequired, @RequestBody(required = false) io.reactivex.Observable rxjava2ObsNotRequired, @RequestBody(required = false) CompletableFuture futureNotRequired, + @RequestBody(required = false) Map mapNotRequired, String notAnnotated) {} }