From cdbcec06cdbc75c5da8e64167366ce4d53cf3c10 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Thu, 5 Jul 2018 13:29:45 -0400 Subject: [PATCH 01/12] Use constant --- .../rewrite/ModifyResponseBodyGatewayFilterFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 cfcf2de1..6089d16f 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 @@ -45,6 +45,8 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.server.ServerWebExchange; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; + /** * This filter is BETA and may be subject to change in a future release. */ @@ -82,7 +84,7 @@ public class ModifyResponseBodyGatewayFilterFactory Class inClass = config.getInClass(); Class outClass = config.getOutClass(); - MediaType originalResponseContentType = exchange.getAttribute("original_response_content_type"); + MediaType originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(originalResponseContentType); ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders); From 884ffa93fcf3b4368450110cf9a025f6245f1023 Mon Sep 17 00:00:00 2001 From: Botorabi Date: Wed, 27 Jun 2018 10:28:43 +0200 Subject: [PATCH 02/12] Fixes typo in method 'convertHttpToWs' The secure WebSocket protocol is "wss", not "wws". --- .../filter/WebsocketRoutingFilter.java | 7 ++-- .../filter/WebscketRoutingFilterTests.java | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java index f8c4c18c..95508ccc 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java @@ -104,7 +104,7 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered { private void changeSchemeIfIsWebSocketUpgrade(ServerWebExchange exchange) { // Check the Upgrade URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); - String scheme = requestUrl.getScheme(); + String scheme = requestUrl.getScheme().toLowerCase(); String upgrade = exchange.getRequest().getHeaders().getUpgrade(); // change the scheme if the socket client send a "http" or "https" if ("WebSocket".equalsIgnoreCase(upgrade) && ("http".equals(scheme) || "https".equals(scheme))) { @@ -117,8 +117,9 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered { } } - private String convertHttpToWs(String scheme) { - return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wws" : scheme; + /* for testing */ static String convertHttpToWs(String scheme) { + scheme = scheme.toLowerCase(); + return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wss" : scheme; } private static class ProxyWebSocketHandler implements WebSocketHandler { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java new file mode 100644 index 00000000..a7e35903 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2018 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 + * + * http://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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.gateway.filter.WebsocketRoutingFilter.convertHttpToWs; + +public class WebscketRoutingFilterTests { + + @Test + public void testConvertHttpToWs() { + assertThat(convertHttpToWs("http")).isEqualTo("ws"); + assertThat(convertHttpToWs("HTTP")).isEqualTo("ws"); + assertThat(convertHttpToWs("https")).isEqualTo("wss"); + assertThat(convertHttpToWs("HTTPS")).isEqualTo("wss"); + assertThat(convertHttpToWs("tcp")).isEqualTo("tcp"); + } +} From 281a6cbb1efee62bc73bb175853a1d050d4b0150 Mon Sep 17 00:00:00 2001 From: Tillmann Heigel Date: Thu, 5 Jul 2018 19:48:06 +0200 Subject: [PATCH 03/12] Fix a small typo in spring-cloud-gateway.adoc (#399) PostGatewayFilterFactory example wasn't compiling. --- docs/src/main/asciidoc/spring-cloud-gateway.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 73a145cb..0621df6e 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -990,7 +990,7 @@ public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory { return chain.filter(exchange).then(Mono.fromRunnable(() -> { - ServerHttpReponse response = exchange.getResponse(); + ServerHttpResponse response = exchange.getResponse(); //Manipulate the response in some way })); }; From 98e76f67851585f4e58cec851a8145062c089154 Mon Sep 17 00:00:00 2001 From: ileler Date: Tue, 3 Jul 2018 10:26:12 +0800 Subject: [PATCH 04/12] Validates Route.uri has a scheme --- .../springframework/cloud/gateway/route/Route.java | 4 +++- .../cloud/gateway/route/RouteTests.java | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java index 42ea5417..58835691 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java @@ -114,7 +114,9 @@ public class Route implements Ordered { public B uri(URI uri) { this.uri = uri; - if (this.uri.getPort() < 0 && this.uri.getScheme().startsWith("http")) { + String scheme = this.uri.getScheme(); + Assert.hasText(scheme, "The parameter [" + this.uri + "] format is incorrect, scheme can not be empty"); + if (this.uri.getPort() < 0 && scheme.startsWith("http")) { // default known http ports int port = this.uri.getScheme().equals("https") ? 443 : 80; this.uri = UriComponentsBuilder.fromUri(this.uri) diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java index 9eec371a..cfdcd33d 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java @@ -17,12 +17,17 @@ package org.springframework.cloud.gateway.route; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import static org.assertj.core.api.Assertions.assertThat; public class RouteTests { + @Rule + public ExpectedException exception = ExpectedException.none(); + @Test public void defeaultHttpPort() { Route route = Route.async().id("1") @@ -47,7 +52,6 @@ public class RouteTests { .hasPort(443); } - @Test public void fullUri() { Route route = Route.async().id("1") @@ -59,4 +63,12 @@ public class RouteTests { .hasScheme("http") .hasPort(8080); } + + @Test + public void nullScheme() { + exception.expect(IllegalArgumentException.class); + Route.async().id("1") + .predicate(exchange -> true) + .uri("/pathonly"); + } } From 93ae55533225ebb0a34e5be898997c86719cf309 Mon Sep 17 00:00:00 2001 From: Tillmann Heigel Date: Thu, 5 Jul 2018 21:40:37 +0200 Subject: [PATCH 05/12] Document the order of the filterchain (#400) (#415) This commit removes the `TODO: document ordering` from spring-cloud-gateway.adoc --- .../main/asciidoc/spring-cloud-gateway.adoc | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 0621df6e..2d02210b 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -781,7 +781,46 @@ The `GlobalFilter` interface has the same signature as `GatewayFilter`. These ar === Combined Global Filter and GatewayFilter Ordering -TODO: document ordering +When a request comes in (and matches a Route) the Filtering Web Handler will add all instances of `GlobalFilter` and all route specific instances of `GatewayFilter` to a filter chain. This combined filter chain is sorted by the `org.springframework.core.Ordered` interface, which can be set by implementing the `getOrder()` method or by using the `@Order` annotation. + +As Spring Cloud Gateway distinguishes between "pre" and "post" phases for filter logic execution (see: How It Works), the filter with the highest precedence will be the first in the "pre"-phase and the last in the "post"-phase. + +.ExampleConfiguration.java +[source,java] +---- +@Bean +@Order(-1) +public GlobalFilter a() { + return (exchange, chain) -> { + log.info("first pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("third post filter"); + })); + }; +} + +@Bean +@Order(0) +public GlobalFilter b() { + return (exchange, chain) -> { + log.info("second pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("second post filter"); + })); + }; +} + +@Bean +@Order(1) +public GlobalFilter c() { + return (exchange, chain) -> { + log.info("third pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("first post filter"); + })); + }; +} +---- === Forward Routing Filter From 0a62617320101a10ebb776ac22faf0411d74b334 Mon Sep 17 00:00:00 2001 From: Arjun Curat Date: Thu, 5 Jul 2018 14:42:50 -0500 Subject: [PATCH 06/12] Added unit tests for ForwardRoutingFilter (#411) --- .../filter/ForwardRoutingFilterTests.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java new file mode 100644 index 00000000..59f8643f --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java @@ -0,0 +1,112 @@ +package org.springframework.cloud.gateway.filter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.Ordered; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +/** + * @author Arjun Curat + */ + +@RunWith(MockitoJUnitRunner.class) +public class ForwardRoutingFilterTests { + + private ServerWebExchange exchange; + + @Mock + private GatewayFilterChain chain; + + @Mock + private DispatcherHandler dispatcherHandler; + + @InjectMocks + private ForwardRoutingFilter forwardRoutingFilter; + + @Before + public void setup() { + exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localendpoint").build()); + } + + @Test + public void shouldNotFilterWhenGatewayRequestUrlSchemeIsNotForward() { + URI uri = UriComponentsBuilder.fromUriString("http://endpoint").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + forwardRoutingFilter.filter(exchange, chain); + + verifyZeroInteractions(dispatcherHandler); + verify(chain).filter(exchange); + verifyNoMoreInteractions(chain); + } + + @Test + public void shouldFilterWhenGatewayRequestUrlSchemeIsForward() { + URI uri = UriComponentsBuilder.fromUriString("forward://endpoint").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + + assertThat(exchange.getAttributes().get(GATEWAY_ALREADY_ROUTED_ATTR)).isNull(); + + forwardRoutingFilter.filter(exchange, chain); + + verifyNoMoreInteractions(chain); + verify(dispatcherHandler).handle(exchange); + + assertThat(exchange.getAttributes().get(GATEWAY_ALREADY_ROUTED_ATTR)).isEqualTo(true); + } + + @Test + public void shouldFilterAndKeepHostPathAsSpecified() { + + URI uri = UriComponentsBuilder.fromUriString("forward://host/outage").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + + forwardRoutingFilter.filter(exchange, chain); + + verify(dispatcherHandler).handle(captor.capture()); + + ServerWebExchange webExchange = captor.getValue(); + + URI forwardedUrl = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + + assertThat(forwardedUrl).hasScheme("forward").hasHost("host").hasPath("/outage"); + + } + + + @Test + public void shouldNotFilterWhenGatewayRequestUrlSchemeIsForwardButAlreadyRouted() { + URI uri = UriComponentsBuilder.fromUriString("forward://host").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + exchange.getAttributes().put(GATEWAY_ALREADY_ROUTED_ATTR, true); + + forwardRoutingFilter.filter(exchange, chain); + + verifyZeroInteractions(dispatcherHandler); + verify(chain).filter(exchange); + verifyNoMoreInteractions(chain); + } + + @Test + public void orderIsLowestPrecedence() { + assertThat(forwardRoutingFilter.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + } + +} From fdef92b3b77f57d0cf2d327bd9d73eac2ae220ee Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Fri, 6 Jul 2018 04:46:11 +0900 Subject: [PATCH 07/12] Autoconfigure CloudFoundryRouteServiceRoutePredicateFactory and add cloudFoundryRouteService in DSL (#294) --- .../config/GatewayAutoConfiguration.java | 6 ++ .../gateway/route/builder/PredicateSpec.java | 7 ++ ...RoutePredicateFactoryIntegrationTests.java | 76 +++++++++++++++++++ .../src/test/resources/application.yml | 8 ++ 4 files changed, 97 insertions(+) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index b936e620..0f14ac9d 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -89,6 +89,7 @@ import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory; @@ -432,6 +433,11 @@ public class GatewayAutoConfiguration { return new WeightRoutePredicateFactory(); } + @Bean + public CloudFoundryRouteServiceRoutePredicateFactory cloudFoundryRouteServiceRoutePredicateFactory() { + return new CloudFoundryRouteServiceRoutePredicateFactory(); + } + // GatewayFilter Factory beans @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index dc805737..993d849a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -23,6 +23,7 @@ import org.springframework.cloud.gateway.handler.AsyncPredicate; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory; @@ -248,6 +249,12 @@ public class PredicateSpec extends UriSpec { .setWeight(weight))); } + public BooleanSpec cloudFoundryRouteService() { + return predicate( + getBean(CloudFoundryRouteServiceRoutePredicateFactory.class).apply(c -> { + })); + } + /** * A predicate which is always true * @return a {@link BooleanSpec} to be used to add logical operators diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java new file mode 100644 index 00000000..14701593 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java @@ -0,0 +1,76 @@ +package org.springframework.cloud.gateway.handler.predicate; + +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.boot.web.server.LocalServerPort; +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.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.fasterxml.jackson.databind.JsonNode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Toshiaki Maki + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests + extends BaseWebClientTests { + @LocalServerPort + int port; + + @Test + public void predicateWorkWithProperties() { + testClient.get().uri("/").header("Host", "props.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Signature", "foo") + .header("X-CF-Proxy-Metadata", "bar").exchange() + .expectBody(JsonNode.class) + .consumeWith(r -> assertThat(r.getResponseBody().has("status")).isTrue()); + } + + @Test + public void predicateWillNotWorkUnlessHeadersAreEnough() { + testClient.get().uri("/").header("Host", "props.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Metadata", "bar").exchange().expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + } + + @Test + public void predicateWorkWithDsl() { + testClient.get().uri("/").header("Host", "dsl.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Signature", "foo") + .header("X-CF-Proxy-Metadata", "bar").exchange() + .expectBody(JsonNode.class) + .consumeWith(r -> assertThat(r.getResponseBody().has("status")).isTrue()); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + @Bean + public RouteLocator routeLocator(RouteLocatorBuilder builder) { + return builder.routes().route(r -> r.cloudFoundryRouteService().and() + .header("Host", "dsl.routeservice.example.com") + .filters(f -> f.requestHeaderToRequestUri("X-CF-Forwarded-Url")) + .uri("http://example.com")).build(); + } + } +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index dc2bb2eb..2db2461e 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -253,6 +253,14 @@ spring: filters: - RequestHeaderToRequestUri=X-CF-Forwarded-Url + # ===================================== + - id: cloudfoundry_routeservice_test + uri: ${test.uri} + predicates: + - CloudFoundryRouteService= + filters: + - RequestHeaderToRequestUri=X-CF-Forwarded-Url + # ===================================== - id: default_path_to_httpbin uri: ${test.uri} From ef173a64c6700a7f8f2d28ba67b4e038955a608e Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Mon, 18 Jun 2018 22:49:00 -0400 Subject: [PATCH 08/12] Adds response timeout using Mono.timeout fixes gh-371 --- .../config/GatewayAutoConfiguration.java | 5 ++-- .../gateway/config/HttpClientProperties.java | 14 ++++++++++ .../gateway/filter/NettyRoutingFilter.java | 22 ++++++++++++--- .../factory/RetryGatewayFilterFactory.java | 10 +++---- .../gateway/support/TimeoutException.java | 28 +++++++++++++++++++ .../config/GatewayAutoConfigurationTests.java | 1 + 6 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 0f14ac9d..1317550a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -217,8 +217,9 @@ public class GatewayAutoConfiguration { @Bean public NettyRoutingFilter routingFilter(HttpClient httpClient, - ObjectProvider> headersFilters) { - return new NettyRoutingFilter(httpClient, headersFilters); + ObjectProvider> headersFilters, + HttpClientProperties properties) { + return new NettyRoutingFilter(httpClient, headersFilters, properties); } @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index ef627a59..da365d5f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -20,6 +20,8 @@ package org.springframework.cloud.gateway.config; import org.springframework.boot.context.properties.ConfigurationProperties; import reactor.ipc.netty.resources.PoolResources; +import java.time.Duration; + /** * Configuration properties for the Netty {@link reactor.ipc.netty.http.client.HttpClient} */ @@ -29,6 +31,9 @@ public class HttpClientProperties { /** The connect timeout in millis, the default is 45s. */ private Integer connectTimeout; + /** The response timeout. */ + private Duration responseTimeout; + /** Pool configuration for Netty HttpClient */ private Pool pool = new Pool(); @@ -42,6 +47,14 @@ public class HttpClientProperties { return connectTimeout; } + public Duration getResponseTimeout() { + return responseTimeout; + } + + public void setResponseTimeout(Duration responseTimeout) { + this.responseTimeout = responseTimeout; + } + public void setConnectTimeout(Integer connectTimeout) { this.connectTimeout = connectTimeout; } @@ -220,6 +233,7 @@ public class HttpClientProperties { public String toString() { return "HttpClientProperties{" + "connectTimeout=" + connectTimeout + + ", responseTimeout=" + responseTimeout + ", pool=" + pool + ", proxy=" + proxy + ", ssl=" + ssl + diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index ab36e983..27be9346 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java @@ -26,10 +26,13 @@ import reactor.core.publisher.Mono; import reactor.ipc.netty.NettyPipeline; import reactor.ipc.netty.http.client.HttpClient; import reactor.ipc.netty.http.client.HttpClientRequest; +import reactor.ipc.netty.http.client.HttpClientResponse; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.Type; +import org.springframework.cloud.gateway.support.TimeoutException; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpHeaders; @@ -55,11 +58,14 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { private final HttpClient httpClient; private final ObjectProvider> headersFilters; + private final HttpClientProperties properties; public NettyRoutingFilter(HttpClient httpClient, - ObjectProvider> headersFilters) { + ObjectProvider> headersFilters, + HttpClientProperties properties) { this.httpClient = httpClient; this.headersFilters = headersFilters; + this.properties = properties; } @Override @@ -93,7 +99,7 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); - return this.httpClient.request(method, url, req -> { + Mono responseMono = this.httpClient.request(method, url, req -> { final HttpClientRequest proxyRequest = req.options(NettyPipeline.SendOptions::flushOnEach) .headers(httpHeaders) .chunkedTransfer(chunkedTransfer) @@ -107,8 +113,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { return proxyRequest.sendHeaders() //I shouldn't need this .send(request.getBody().map(dataBuffer -> - ((NettyDataBuffer)dataBuffer).getNativeBuffer())); - }).doOnNext(res -> { + ((NettyDataBuffer) dataBuffer).getNativeBuffer())); + }); + + if (properties.getResponseTimeout() != null) { + responseMono.timeout(properties.getResponseTimeout(), + Mono.error(new TimeoutException("Response took longer than timeout: " + + properties.getResponseTimeout()))); + } + + return responseMono.doOnNext(res -> { ServerHttpResponse response = exchange.getResponse(); // put headers and status so filters can modify the response HttpHeaders headers = new HttpHeaders(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 47a6f735..3dff1d6c 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -26,6 +26,7 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.springframework.cloud.gateway.support.TimeoutException; import reactor.core.publisher.Mono; import reactor.retry.Repeat; import reactor.retry.RepeatContext; @@ -146,12 +147,11 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory List toList(T item) { - ArrayList list = new ArrayList<>(); - list.add(item); - return list; + private static List toList(T... items) { + return new ArrayList<>(Arrays.asList(items)); } + @SuppressWarnings("unchecked") public static class RetryConfig { private int retries = 3; @@ -161,7 +161,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory methods = toList(HttpMethod.GET); - private List> exceptions = toList(IOException.class); + private List> exceptions = toList(IOException.class, TimeoutException.class); public RetryConfig setRetries(int retries) { this.retries = retries; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java new file mode 100644 index 00000000..8d429bc2 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.support; + +public class TimeoutException extends Exception { + + public TimeoutException() { + } + + public TimeoutException(String message) { + super(message); + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index 7161b145..94a1b429 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -62,6 +62,7 @@ public class GatewayAutoConfigurationTests { GatewayAutoConfiguration.class)) .withPropertyValues("spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true", "spring.cloud.gateway.httpclient.connect-timeout=10", + "spring.cloud.gateway.httpclient.response-timeout=10s", "spring.cloud.gateway.httpclient.pool.type=fixed", "spring.cloud.gateway.httpclient.proxy.host=myhost") .run(context -> { From 6a4f60820480fae1d7ce72314cedcc811452686b Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 6 Jul 2018 16:40:54 -0400 Subject: [PATCH 09/12] Bump versions --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 2c19478a..b2d76650 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ org.springframework.cloud spring-cloud-build - 2.0.2.RELEASE + 2.0.2.BUILD-SNAPSHOT @@ -48,8 +48,8 @@ UTF-8 UTF-8 1.8 - 2.0.0.BUILD-SNAPSHOT - 2.0.0.BUILD-SNAPSHOT + 2.0.1.BUILD-SNAPSHOT + 2.0.1.BUILD-SNAPSHOT From 6ceb65b7cfbda9cddf146976e325394ab7d387f7 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 6 Jul 2018 16:42:12 -0400 Subject: [PATCH 10/12] Checks for null list if there are no query parameters. fixes gh-374 --- .../handler/RoutePredicateHandlerMapping.java | 7 +- .../predicate/QueryRoutePredicateFactory.java | 3 + .../QueryRoutePredicateFactoryTests.java | 86 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java index 53fdf340..70d5a67b 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java @@ -92,7 +92,12 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { .filterWhen(route -> { // add the current route we are testing exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId()); - return route.getPredicate().apply(exchange); + try { + return route.getPredicate().apply(exchange); + } catch (Exception e) { + logger.error("Error applying predicate for route: "+route.getId(), e); + } + return Mono.just(false); }) // .defaultIfEmpty() put a static Route not found // or .switchIfEmpty() diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java index a4c38c0d..fb203a10 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java @@ -54,6 +54,9 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory values = exchange.getRequest().getQueryParams().get(config.param); + if (values == null) { + return false; + } for (String value : values) { if (value.matches(config.regexp)) { return true; diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java new file mode 100644 index 00000000..600df02e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.handler.predicate; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.boot.test.rule.OutputCapture; +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.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class QueryRoutePredicateFactoryTests extends BaseWebClientTests { + + @Rule + public OutputCapture output = new OutputCapture(); + + @Test + public void noQueryParamWorks() { + testClient.get().uri("/get") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");; + + output.expect(not(containsString("Error applying predicate for route: foo_query_param"))); + } + + @Test + public void queryParamWorks() { + testClient.get().uri("/get?foo=bar") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "foo_query_param");; + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + private String uri; + + @Bean + RouteLocator queryRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("foo_query_param", r -> + r.query("foo", "bar") + .filters(f -> f.prefixPath("/httpbin")) + .uri(uri)) + .build(); + } + } + +} From c343b0ceec13e8df605c69b01364b56cd891c85f Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Mon, 9 Jul 2018 13:09:26 -0400 Subject: [PATCH 11/12] Checks for null value if query parameter does not have a value fixes gh-423 --- .../handler/predicate/QueryRoutePredicateFactory.java | 2 +- .../predicate/QueryRoutePredicateFactoryTests.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java index fb203a10..c7e38ccb 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java @@ -58,7 +58,7 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory Date: Wed, 11 Jul 2018 12:15:28 +0200 Subject: [PATCH 12/12] Improve route predicate handling of errors in filterWhen This commit slightly changes the way filterWhen is used in route predicate mapping, in order to avoid swallowing of errors due to the combination of `filterWhen` and `next()`. This is due to `filterWhen` delaying the emission of async predicate errors to the end of the Flux, and `next()` potentially cancelling said Flux before the error(s) are propagated. The fix involves applying `filterWhen` inside a `concatMap`, on a value per value basis rather than on the whole original Flux. This results in predicate errors being propagated as they happen, at which point we can log them and swallow them in the concatMap. --- .../handler/RoutePredicateHandlerMapping.java | 29 +++--- .../RoutePredicateHandlerMappingTest.java | 97 +++++++++++++++++++ 2 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java index 70d5a67b..d8212eea 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java @@ -19,6 +19,8 @@ package org.springframework.cloud.gateway.handler; import java.util.function.Function; +import reactor.core.publisher.Mono; + import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.web.cors.CorsConfiguration; @@ -29,8 +31,6 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; -import reactor.core.publisher.Mono; - /** * @author Spencer Gibb */ @@ -88,17 +88,20 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { } protected Mono lookupRoute(ServerWebExchange exchange) { - return this.routeLocator.getRoutes() - .filterWhen(route -> { - // add the current route we are testing - exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId()); - try { - return route.getPredicate().apply(exchange); - } catch (Exception e) { - logger.error("Error applying predicate for route: "+route.getId(), e); - } - return Mono.just(false); - }) + return this.routeLocator + .getRoutes() + //individually filter routes so that filterWhen error delaying is not a problem + .concatMap(route -> Mono + .just(route) + .filterWhen(r -> { + // add the current route we are testing + exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId()); + return r.getPredicate().apply(exchange); + }) + //instead of immediately stopping main flux due to error, log and swallow it + .doOnError(e -> logger.error("Error applying predicate for route: "+route.getId(), e)) + .onErrorResume(e -> Mono.empty()) + ) // .defaultIfEmpty() put a static Route not found // or .switchIfEmpty() // .switchIfEmpty(Mono.empty().log("noroute")) diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java new file mode 100644 index 00000000..49773462 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java @@ -0,0 +1,97 @@ +package org.springframework.cloud.gateway.handler; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.hamcrest.Matchers.containsString; + +/** + * @author Simon Baslé + */ +public class RoutePredicateHandlerMappingTest { + + @Rule + public OutputCapture outputCapture = new OutputCapture(); + + @Test + public void lookupRouteFromSyncPredicates() { + Route routeFalse = Route.async() + .id("routeFalse") + .uri("http://localhost") + .predicate(swe -> false) + .build(); + Route routeFail = Route.async() + .id("routeFail") + .uri("http://localhost") + .predicate(swe -> { throw new IllegalStateException("boom"); }) + .build(); + Route routeTrue = Route.async() + .id("routeTrue") + .uri("http://localhost") + .predicate(swe -> true) + .build(); + RouteLocator routeLocator = + () -> Flux.just(routeFalse, routeFail, routeTrue).hide(); + RoutePredicateHandlerMapping mapping = + new RoutePredicateHandlerMapping(null, routeLocator); + + final Mono routeMono = + mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); + + StepVerifier.create(routeMono.map(Route::getId)) + .expectNext("routeTrue") + .verifyComplete(); + + outputCapture.expect(containsString("Error applying predicate for route: routeFail")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom")); + } + + @Test + public void lookupRouteFromAsyncPredicates() { + Route routeFalse = Route.async() + .id("routeFalse") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.just(false)) + .build(); + Route routeError = Route.async() + .id("routeError") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.error(new IllegalStateException("boom1"))) + .build(); + Route routeFail = Route.async() + .id("routeFail") + .uri("http://localhost") + .asyncPredicate(swe -> { throw new IllegalStateException("boom2"); }) + .build(); + Route routeTrue = Route.async() + .id("routeTrue") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.just(true)) + .build(); + RouteLocator routeLocator = + () -> Flux.just(routeFalse, routeError, routeFail, routeTrue).hide(); + RoutePredicateHandlerMapping mapping = + new RoutePredicateHandlerMapping(null, routeLocator); + + final Mono routeMono = + mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); + + StepVerifier.create(routeMono.map(Route::getId)) + .expectNext("routeTrue") + .verifyComplete(); + + outputCapture.expect(containsString("Error applying predicate for route: routeError")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom1")); + + outputCapture.expect(containsString("Error applying predicate for route: routeFail")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom2")); + } +} \ No newline at end of file