Merge branch '2.2.x'

This commit is contained in:
Spencer Gibb
2020-02-28 01:35:19 -05:00
17 changed files with 596 additions and 59 deletions

View File

@@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
@@ -56,6 +57,12 @@ public class GatewayProperties {
private List<MediaType> streamingMediaTypes = Arrays
.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);
/**
* Option to fail on route definition errors, defaults to true. Otherwise, a warning
* is logged.
*/
private boolean failOnRouteDefinitionError = true;
public List<RouteDefinition> getRoutes() {
return routes;
}
@@ -83,10 +90,22 @@ public class GatewayProperties {
this.streamingMediaTypes = streamingMediaTypes;
}
public boolean isFailOnRouteDefinitionError() {
return failOnRouteDefinitionError;
}
public void setFailOnRouteDefinitionError(boolean failOnRouteDefinitionError) {
this.failOnRouteDefinitionError = failOnRouteDefinitionError;
}
@Override
public String toString() {
return "GatewayProperties{" + "routes=" + routes + ", defaultFilters="
+ defaultFilters + ", streamingMediaTypes=" + streamingMediaTypes + '}';
return new ToStringCreator(this).append("routes", routes)
.append("defaultFilters", defaultFilters)
.append("streamingMediaTypes", streamingMediaTypes)
.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
.toString();
}
}

View File

@@ -252,17 +252,38 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
* @return
*/
protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
Integer connectTimeout = (Integer) route.getMetadata().get(CONNECT_TIMEOUT_ATTR);
if (connectTimeout != null) {
Object connectTimeoutAttr = route.getMetadata().get(CONNECT_TIMEOUT_ATTR);
if (connectTimeoutAttr != null) {
Integer connectTimeout = getInteger(connectTimeoutAttr);
return this.httpClient.tcpConfiguration((tcpClient) -> tcpClient
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout));
}
return httpClient;
}
static Integer getInteger(Object connectTimeoutAttr) {
Integer connectTimeout;
if (connectTimeoutAttr instanceof Integer) {
connectTimeout = (Integer) connectTimeoutAttr;
}
else {
connectTimeout = Integer.parseInt(connectTimeoutAttr.toString());
}
return connectTimeout;
}
private Duration getResponseTimeout(Route route) {
Number responseTimeout = (Number) route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
return responseTimeout != null ? Duration.ofMillis(responseTimeout.longValue())
Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR);
Long responseTimeout = null;
if (responseTimeoutAttr != null) {
if (responseTimeoutAttr instanceof Number) {
responseTimeout = ((Number) responseTimeoutAttr).longValue();
}
else {
responseTimeout = Long.valueOf(responseTimeoutAttr.toString());
}
}
return responseTimeout != null ? Duration.ofMillis(responseTimeout)
: properties.getResponseTimeout();
}

View File

@@ -71,8 +71,10 @@ public class ModifyRequestBodyGatewayFilterFactory extends
// TODO: flux or mono
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass)
// .log("modify_request_mono", Level.INFO)
.flatMap(o -> config.rewriteFunction.apply(exchange, o));
.flatMap(originalBody -> config.getRewriteFunction()
.apply(exchange, originalBody))
.switchIfEmpty(Mono.defer(() -> (Mono) config.getRewriteFunction()
.apply(exchange, null)));
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
config.getOutClass());
@@ -117,7 +119,7 @@ public class ModifyRequestBodyGatewayFilterFactory extends
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.putAll(headers);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
}

View File

@@ -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);

View File

@@ -50,18 +50,24 @@ public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> Mono.justOrEmpty(routeDefinition.getId())
.defaultIfEmpty(idGenerator.generateId().toString())
.publishOn(Schedulers.elastic()).map(id -> {
if (routeDefinition.getId() == null) {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug("Id set on route definition: "
+ routeDefinition);
}
.flatMap(routeDefinition -> {
if (routeDefinition.getId() == null) {
return randomId().map(id -> {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug(
"Id set on route definition: " + routeDefinition);
}
return routeDefinition;
}));
});
}
return Mono.just(routeDefinition);
});
}
protected Mono<String> randomId() {
return Mono.fromSupplier(idGenerator::toString)
.publishOn(Schedulers.boundedElastic());
}
}

View File

@@ -143,19 +143,26 @@ public class RouteDefinitionRouteLocator
@Override
public Flux<Route> getRoutes() {
return this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute)
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions()
.map(this::convertToRoute);
/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
if (!gatewayProperties.isFailOnRouteDefinitionError()) {
// instead of letting error bubble up, continue
routes = routes.onErrorContinue((error, obj) -> {
if (logger.isWarnEnabled()) {
logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
+ " will be ignored. Definition has invalid configs, "
+ error.getMessage());
}
});
}
return routes.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}
private Route convertToRoute(RouteDefinition routeDefinition) {

View File

@@ -223,8 +223,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
@@ -240,8 +239,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
@@ -258,9 +256,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 <T> the original request body class
* @param <R> the new request body class
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
* <pre>
* {@code
@@ -281,8 +280,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
@@ -299,8 +297,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
@@ -321,9 +318,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 <T> the original response body class
* @param <R> the new response body class
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
* <pre>
* {@code
@@ -478,7 +476,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

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.filter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* This test just avoid class cast exception with YAML or Properties parsing.
*
* {@link NettyRoutingFilter#getHttpClient(Route, ServerWebExchange)}
* {@link NettyRoutingFilter#getResponseTimeout(Route)}
*
* @author echooymxq
**/
@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "spring.cloud.gateway.routes[0].id=route_connect_timeout",
"spring.cloud.gateway.routes[0].uri=http://localhost:32167",
"spring.cloud.gateway.routes[0].predicates[0].name=Path",
"spring.cloud.gateway.routes[0].predicates[0].args[pattern]=/connect/delay/{timeout}",
"spring.cloud.gateway.routes[0].metadata[connect-timeout]=5",
"spring.cloud.gateway.routes[1].id=route_response_timeout",
"spring.cloud.gateway.routes[1].uri=lb://testservice",
"spring.cloud.gateway.routes[1].predicates[0].name=Path",
"spring.cloud.gateway.routes[1].predicates[0].args[pattern]=/route/delay/{timeout}",
"spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1",
"spring.cloud.gateway.routes[1].metadata.response-timeout=1000" },
webEnvironment = RANDOM_PORT)
@DirtiesContext
public class NettyRoutingFilterCompatibleTests extends BaseWebClientTests {
@Test
public void shouldApplyConnectTimeoutPerRoute() {
assertThat(NettyRoutingFilter.getInteger("5")).isEqualTo(5);
assertThat(NettyRoutingFilter.getInteger(5)).isEqualTo(5);
}
@Test
public void shouldApplyResponseTimeoutPerRoute() {
testClient.get().uri("/route/delay/2").exchange().expectStatus()
.isEqualTo(HttpStatus.GATEWAY_TIMEOUT).expectBody().jsonPath("$.status")
.isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value()))
.jsonPath("$.message")
.isEqualTo("Response took longer than timeout: PT1S");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.filter.factory.rewrite;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.BodyInserters;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* @author Junghoon Song
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class ModifyRequestBodyGatewayFilterFactoryTests extends BaseWebClientTests {
@Test
public void modifyRequestBody() {
testClient.post().uri("/post").header("Host", "www.modifyrequestbody.org")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.body(BodyInserters.fromValue("request")).exchange().expectStatus()
.isEqualTo(HttpStatus.OK).expectBody().jsonPath("headers.Content-Type")
.isEqualTo(MediaType.APPLICATION_JSON_VALUE).jsonPath("data")
.isEqualTo("modifyrequest");
}
@Test
public void upstreamRequestBodyIsEmpty() {
testClient.post().uri("/post").header("Host", "www.modifyrequestbodyempty.org")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.exchange().expectStatus().isEqualTo(HttpStatus.OK).expectBody()
.jsonPath("headers.Content-Type")
.isEqualTo(MediaType.APPLICATION_JSON_VALUE).jsonPath("data")
.isEqualTo("modifyrequest");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
@Value("${test.uri}")
String uri;
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("test_modify_request_body",
r -> r.order(-1).host("**.modifyrequestbody.org")
.filters(f -> f.modifyRequestBody(String.class, String.class,
MediaType.APPLICATION_JSON_VALUE,
(serverWebExchange, aVoid) -> {
return Mono.just("modifyrequest");
}))
.uri(uri))
.route("test_modify_request_body_empty", r -> r.order(-1)
.host("**.modifyrequestbodyempty.org")
.filters(f -> f.modifyRequestBody(String.class, String.class,
MediaType.APPLICATION_JSON_VALUE,
(serverWebExchange, body) -> {
if (body == null) {
return Mono.just("modifyrequest");
}
return Mono.just(body.toUpperCase());
}))
.uri(uri))
.build();
}
}
}

View File

@@ -21,6 +21,8 @@ import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.config.PropertiesRouteDefinitionLocator;
@@ -34,7 +36,7 @@ import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGate
import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -66,19 +68,70 @@ public class RouteDefinitionRouteLocatorTests {
}
}));
PropertiesRouteDefinitionLocator routeDefinitionLocator = new PropertiesRouteDefinitionLocator(
gatewayProperties);
@SuppressWarnings("deprecation")
RouteDefinitionRouteLocator routeDefinitionRouteLocator = new RouteDefinitionRouteLocator(
new PropertiesRouteDefinitionLocator(gatewayProperties), predicates,
gatewayFilterFactories, gatewayProperties,
new DefaultConversionService());
new CompositeRouteDefinitionLocator(Flux.just(routeDefinitionLocator)),
predicates, gatewayFilterFactories, gatewayProperties,
new ConfigurationService());
List<Route> routes = routeDefinitionRouteLocator.getRoutes().collectList()
.block();
List<GatewayFilter> filters = routes.get(0).getFilters();
assertThat(filters).hasSize(3);
assertThat(getFilterClassName(filters.get(0))).contains("RemoveResponseHeader");
assertThat(getFilterClassName(filters.get(1))).contains("AddResponseHeader");
assertThat(getFilterClassName(filters.get(2)))
.contains("RouteDefinitionRouteLocatorTests$TestOrderedGateway");
StepVerifier.create(routeDefinitionRouteLocator.getRoutes()).assertNext(route -> {
List<GatewayFilter> filters = route.getFilters();
assertThat(filters).hasSize(3);
assertThat(getFilterClassName(filters.get(0)))
.contains("RemoveResponseHeader");
assertThat(getFilterClassName(filters.get(1))).contains("AddResponseHeader");
assertThat(getFilterClassName(filters.get(2)))
.contains("RouteDefinitionRouteLocatorTests$TestOrderedGateway");
}).expectComplete().verify();
}
@Test
public void contextLoadsWithErrorRecovery() {
List<RoutePredicateFactory> predicates = Arrays
.asList(new HostRoutePredicateFactory());
List<GatewayFilterFactory> gatewayFilterFactories = Arrays.asList(
new RemoveResponseHeaderGatewayFilterFactory(),
new AddResponseHeaderGatewayFilterFactory(),
new TestOrderedGatewayFilterFactory());
GatewayProperties gatewayProperties = new GatewayProperties();
gatewayProperties.setRoutes(containsInvalidRoutes());
gatewayProperties.setFailOnRouteDefinitionError(false);
PropertiesRouteDefinitionLocator routeDefinitionLocator = new PropertiesRouteDefinitionLocator(
gatewayProperties);
@SuppressWarnings("deprecation")
RouteDefinitionRouteLocator routeDefinitionRouteLocator = new RouteDefinitionRouteLocator(
new CompositeRouteDefinitionLocator(Flux.just(routeDefinitionLocator)),
predicates, gatewayFilterFactories, gatewayProperties,
new ConfigurationService());
StepVerifier.create(routeDefinitionRouteLocator.getRoutes()).assertNext(route -> {
List<GatewayFilter> filters = route.getFilters();
assertThat(filters).hasSize(3);
assertThat(getFilterClassName(filters.get(0)))
.contains("RemoveResponseHeader");
assertThat(getFilterClassName(filters.get(1))).contains("AddResponseHeader");
assertThat(getFilterClassName(filters.get(2)))
.contains("RouteDefinitionRouteLocatorTests$TestOrderedGateway");
}).expectComplete().verify();
}
private List<RouteDefinition> containsInvalidRoutes() {
RouteDefinition foo = new RouteDefinition();
foo.setId("foo");
foo.setUri(URI.create("https://foo.example.com"));
foo.setPredicates(Arrays.asList(new PredicateDefinition("Host=*.example.com")));
foo.setFilters(Arrays.asList(new FilterDefinition("RemoveResponseHeader=Server"),
new FilterDefinition("TestOrdered="),
new FilterDefinition("AddResponseHeader=X-Response-Foo, Bar")));
RouteDefinition bad = new RouteDefinition();
bad.setId("exceptionRaised");
bad.setUri(URI.create("https://foo.example.com"));
bad.setPredicates(Arrays.asList(new PredicateDefinition("Host=*.example.com")));
bad.setFilters(Arrays.asList(new FilterDefinition("Generate exception")));
return Arrays.asList(foo, bad);
}
private String getFilterClassName(GatewayFilter target) {

View File

@@ -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

View File

@@ -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<String> emptyResponse() {
return Mono.empty();
}
public Map<String, String> getHeaders(ServerWebExchange exchange) {
return exchange.getRequest().getHeaders().toSingleValueMap();
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.test;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
public class RouteConstructionIntegrationTests {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void routesWithVerificationShouldFail() {
exception.expect(Throwable.class);
new SpringApplicationBuilder(TestConfig.class).profiles("verification-route")
.run();
}
@EnableAutoConfiguration
@SpringBootConfiguration
public static class TestConfig {
@Bean
public TestFilterGatewayFilterFactory testFilterGatewayFilterFactory() {
return new TestFilterGatewayFilterFactory();
}
}
public static class TestFilterGatewayFilterFactory
extends AbstractGatewayFilterFactory<TestFilterGatewayFilterFactory.Config> {
public TestFilterGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
throw new AssertionError("Stop right now!");
}
public static class Config {
private String arg1;
public String getArg1() {
return arg1;
}
public void setArg1(String arg1) {
this.arg1 = arg1;
}
}
}
}

View File

@@ -0,0 +1,11 @@
spring:
cloud:
gateway:
routes:
- uri: https://example.com
predicates:
- Path=/verification/**
filters:
- name: TestFilter
args:
arg1: world

View File

@@ -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")

View File

@@ -128,6 +128,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() {

View File

@@ -350,7 +350,7 @@ public class ProductionConfigurationTests {
ProxyExchange<List<Object>> proxy) throws Exception {
body.put("id", id);
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(body))
.post(this::first);
.forward(this::first);
}
}