Merge branch '2.2.x'
This commit is contained in:
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- uri: https://example.com
|
||||
predicates:
|
||||
- Path=/verification/**
|
||||
filters:
|
||||
- name: TestFilter
|
||||
args:
|
||||
arg1: world
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user