diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index 6fb6ef05..0e4d7fda 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -203,8 +203,10 @@ public class ModifyResponseBodyGatewayFilterFactory extends // TODO: flux or mono Mono modifiedBody = clientResponse.bodyToMono(inClass) - .flatMap(originalBody -> config.rewriteFunction - .apply(exchange, originalBody)); + .flatMap(originalBody -> config.getRewriteFunction() + .apply(exchange, originalBody)) + .switchIfEmpty(Mono.defer(() -> (Mono) config + .getRewriteFunction().apply(exchange, null))); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index df8aca99..9871d5cc 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -246,8 +246,7 @@ public class GatewayFilterSpec extends UriSpec { } /** - * A filter that can be used to modify the request body. This filter is BETA and may - * be subject to change in a future release. + * A filter that can be used to modify the request body. * @param inClass the class to convert the incoming request body to * @param outClass the class the Gateway will add to the request before it is routed * @param rewriteFunction the {@link RewriteFunction} that transforms the request body @@ -263,8 +262,7 @@ public class GatewayFilterSpec extends UriSpec { } /** - * A filter that can be used to modify the request body. This filter is BETA and may - * be subject to change in a future release. + * A filter that can be used to modify the request body. * @param inClass the class to convert the incoming request body to * @param outClass the class the Gateway will add to the request before it is routed * @param newContentType the new Content-Type header to be sent @@ -281,9 +279,10 @@ public class GatewayFilterSpec extends UriSpec { } /** - * A filter that can be used to modify the request body. This filter is BETA and may - * be subject to change in a future release. + * A filter that can be used to modify the request body. * @param configConsumer request spec for response modification + * @param the original request body class + * @param the new request body class * @return a {@link GatewayFilterSpec} that can be used to apply additional filters *
 	 * {@code
@@ -304,8 +303,7 @@ public class GatewayFilterSpec extends UriSpec {
 	}
 
 	/**
-	 * A filter that can be used to modify the response body This filter is BETA and may
-	 * be subject to change in a future release.
+	 * A filter that can be used to modify the response body.
 	 * @param inClass the class to conver the response body to
 	 * @param outClass the class the Gateway will add to the response before it is
 	 * returned to the client
@@ -322,8 +320,7 @@ public class GatewayFilterSpec extends UriSpec {
 	}
 
 	/**
-	 * A filter that can be used to modify the response body This filter is BETA and may
-	 * be subject to change in a future release.
+	 * A filter that can be used to modify the response body.
 	 * @param inClass the class to conver the response body to
 	 * @param outClass the class the Gateway will add to the response before it is
 	 * returned to the client
@@ -344,9 +341,10 @@ public class GatewayFilterSpec extends UriSpec {
 	}
 
 	/**
-	 * A filter that can be used to modify the response body using custom spec. This
-	 * filter is BETA and may be subject to change in a future release.
+	 * A filter that can be used to modify the response body using custom spec.
 	 * @param configConsumer response spec for response modification
+	 * @param  the original response body class
+	 * @param  the new response body class
 	 * @return a {@link GatewayFilterSpec} that can be used to apply additional filters
 	 * 
 	 * {@code
@@ -501,7 +499,7 @@ public class GatewayFilterSpec extends UriSpec {
 	}
 
 	/**
-	 * A filter which rewrites the request path before it is routed by the Gateway
+	 * A filter which rewrites the request path before it is routed by the Gateway.
 	 * @param regex a Java regular expression to match the path against
 	 * @param replacement the replacement for the path
 	 * @return a {@link GatewayFilterSpec} that can be used to apply additional filters
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpecTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpecTests.java
index c9e1fe44..871d7834 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpecTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpecTests.java
@@ -21,14 +21,18 @@ import reactor.core.publisher.Mono;
 
 import org.springframework.cloud.gateway.filter.GatewayFilter;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
 import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
 import org.springframework.cloud.gateway.route.Route;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.Ordered;
+import org.springframework.http.MediaType;
 import org.springframework.web.server.ServerWebExchange;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class GatewayFilterSpecTests {
 
@@ -81,6 +85,100 @@ public class GatewayFilterSpecTests {
 		assertFilter(route.getFilters().get(1), MyOrderedFilter.class, 1000);
 	}
 
+	@Test
+	public void shouldSetModifyBodyResponseFilterWithRewriteFunction() {
+		ConfigurableApplicationContext context = mock(
+				ConfigurableApplicationContext.class);
+		Route.AsyncBuilder routeBuilder = Route.async().id("123").uri("abc:123")
+				.predicate(exchange -> true);
+
+		when(context.getBean(ModifyResponseBodyGatewayFilterFactory.class))
+				.thenReturn(new ModifyResponseBodyGatewayFilterFactory());
+
+		RouteLocatorBuilder.Builder routes = new RouteLocatorBuilder(context).routes();
+		GatewayFilterSpec spec = new GatewayFilterSpec(routeBuilder, routes);
+		spec.modifyResponseBody(String.class, String.class,
+				(exchange, s) -> Mono.just(s));
+
+		Route route = routeBuilder.build();
+		assertThat(route.getFilters()).hasSize(1);
+
+		assertFilter(route.getFilters().get(0),
+				ModifyResponseBodyGatewayFilterFactory.ModifyResponseGatewayFilter.class,
+				NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1);
+	}
+
+	@Test
+	public void shouldSetModifyBodyResponseFilterWithRewriteFunctionAndEmptyBodySupplier() {
+		ConfigurableApplicationContext context = mock(
+				ConfigurableApplicationContext.class);
+		Route.AsyncBuilder routeBuilder = Route.async().id("123").uri("abc:123")
+				.predicate(exchange -> true);
+
+		when(context.getBean(ModifyResponseBodyGatewayFilterFactory.class))
+				.thenReturn(new ModifyResponseBodyGatewayFilterFactory());
+
+		RouteLocatorBuilder.Builder routes = new RouteLocatorBuilder(context).routes();
+		GatewayFilterSpec spec = new GatewayFilterSpec(routeBuilder, routes);
+		spec.modifyResponseBody(String.class, String.class,
+				(exchange, s) -> Mono.just(s == null ? "emptybody" : s));
+
+		Route route = routeBuilder.build();
+		assertThat(route.getFilters()).hasSize(1);
+
+		assertFilter(route.getFilters().get(0),
+				ModifyResponseBodyGatewayFilterFactory.ModifyResponseGatewayFilter.class,
+				NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1);
+	}
+
+	@Test
+	public void shouldSetModifyBodyResponseFilterWithRewriteFunctionAndNewContentType() {
+		ConfigurableApplicationContext context = mock(
+				ConfigurableApplicationContext.class);
+		Route.AsyncBuilder routeBuilder = Route.async().id("123").uri("abc:123")
+				.predicate(exchange -> true);
+
+		when(context.getBean(ModifyResponseBodyGatewayFilterFactory.class))
+				.thenReturn(new ModifyResponseBodyGatewayFilterFactory());
+
+		RouteLocatorBuilder.Builder routes = new RouteLocatorBuilder(context).routes();
+		GatewayFilterSpec spec = new GatewayFilterSpec(routeBuilder, routes);
+		spec.modifyResponseBody(String.class, String.class,
+				MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> Mono.just(s));
+
+		Route route = routeBuilder.build();
+		assertThat(route.getFilters()).hasSize(1);
+
+		assertFilter(route.getFilters().get(0),
+				ModifyResponseBodyGatewayFilterFactory.ModifyResponseGatewayFilter.class,
+				NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1);
+	}
+
+	@Test
+	public void shouldSetModifyBodyResponseFilterWithConfigConsumer() {
+		ConfigurableApplicationContext context = mock(
+				ConfigurableApplicationContext.class);
+		Route.AsyncBuilder routeBuilder = Route.async().id("123").uri("abc:123")
+				.predicate(exchange -> true);
+
+		when(context.getBean(ModifyResponseBodyGatewayFilterFactory.class))
+				.thenReturn(new ModifyResponseBodyGatewayFilterFactory());
+
+		RouteLocatorBuilder.Builder routes = new RouteLocatorBuilder(context).routes();
+		GatewayFilterSpec spec = new GatewayFilterSpec(routeBuilder, routes);
+		spec.modifyResponseBody(
+				(smth) -> new ModifyResponseBodyGatewayFilterFactory.Config()
+						.setRewriteFunction(String.class, String.class,
+								(exchange, s) -> Mono.just(s)));
+
+		Route route = routeBuilder.build();
+		assertThat(route.getFilters()).hasSize(1);
+
+		assertFilter(route.getFilters().get(0),
+				ModifyResponseBodyGatewayFilterFactory.ModifyResponseGatewayFilter.class,
+				NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1);
+	}
+
 	protected static class MyOrderedFilter implements GatewayFilter, Ordered {
 
 		@Override
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java
index fa2438bd..530c64e0 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java
@@ -147,6 +147,12 @@ public class HttpBinCompatibleController {
 		return ResponseEntity.status(status).body("Failed with " + status);
 	}
 
+	@RequestMapping(path = "/post/empty", method = RequestMethod.POST,
+			produces = MediaType.APPLICATION_JSON_VALUE)
+	public Mono emptyResponse() {
+		return Mono.empty();
+	}
+
 	public Map getHeaders(ServerWebExchange exchange) {
 		return exchange.getRequest().getHeaders().toSingleValueMap();
 	}
diff --git a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java
index 6e2bcf4b..cb0b194d 100644
--- a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java
+++ b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java
@@ -99,6 +99,31 @@ public class GatewaySampleApplication {
 									})
 					).uri(uri)
 				)
+				.route("rewrite_empty_response", r -> r.host("*.rewriteemptyresponse.org")
+					.filters(f -> f.prefixPath("/httpbin")
+							.addResponseHeader("X-TestHeader", "rewrite_empty_response")
+							.modifyResponseBody(String.class, String.class,
+									(exchange, s) -> {
+										if (s == null) {
+											return Mono.just("emptybody");
+										}
+										return Mono.just(s.toUpperCase());
+									})
+
+					).uri(uri)
+				)
+				.route("rewrite_response_fail_supplier", r -> r.host("*.rewriteresponsewithfailsupplier.org")
+					.filters(f -> f.prefixPath("/httpbin")
+							.addResponseHeader("X-TestHeader", "rewrite_response_fail_supplier")
+							.modifyResponseBody(String.class, String.class,
+									(exchange, s) -> {
+										if (s == null) {
+											return Mono.error(new IllegalArgumentException("this should not happen"));
+										}
+										return Mono.just(s.toUpperCase());
+									})
+					).uri(uri)
+				)
 				.route("rewrite_response_obj", r -> r.host("*.rewriteresponseobj.org")
 					.filters(f -> f.prefixPath("/httpbin")
 							.addResponseHeader("X-TestHeader", "rewrite_response_obj")
diff --git a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java
index 1b0127b8..61099b56 100644
--- a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java
+++ b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java
@@ -130,6 +130,29 @@ public class GatewaySampleApplicationTests {
 						.containsEntry("DATA", "HELLO"));
 	}
 
+	@Test
+	@SuppressWarnings("unchecked")
+	public void rewriteResponseEmptyBodyToStringWorks() {
+		webClient.post().uri("/post/empty").header("Host", "www.rewriteemptyresponse.org")
+				.exchange().expectStatus().isOk().expectHeader()
+				.valueEquals("X-TestHeader", "rewrite_empty_response")
+				.expectBody(String.class)
+				.consumeWith(result -> assertThat(result.getResponseBody())
+						.isEqualTo("emptybody"));
+	}
+
+	@Test
+	@SuppressWarnings("unchecked")
+	public void emptyBodySupplierNotCalledWhenBodyPresent() {
+		webClient.post().uri("/post")
+				.header("Host", "www.rewriteresponsewithfailsupplier.org")
+				.bodyValue("hello").exchange().expectStatus().isOk().expectHeader()
+				.valueEquals("X-TestHeader", "rewrite_response_fail_supplier")
+				.expectBody(Map.class)
+				.consumeWith(result -> assertThat(result.getResponseBody())
+						.containsEntry("DATA", "HELLO"));
+	}
+
 	@Test
 	@SuppressWarnings("unchecked")
 	public void rewriteResponeBodyObjectWorks() {