diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index f094ee3c..46349462 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -49,7 +49,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: after_route uri: http://example.org predicates: @@ -68,7 +67,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: before_route uri: http://example.org predicates: @@ -87,7 +85,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: between_route uri: http://example.org predicates: @@ -106,7 +103,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: cookie_route uri: http://example.org predicates: @@ -125,7 +121,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: header_route uri: http://example.org predicates: @@ -144,7 +139,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: host_route uri: http://example.org predicates: @@ -164,7 +158,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: method_route uri: http://example.org predicates: @@ -183,7 +176,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: host_route uri: http://example.org predicates: @@ -204,7 +196,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: query_route uri: http://example.org predicates: @@ -220,7 +211,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: query_route uri: http://example.org predicates: @@ -240,7 +230,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: remoteaddr_route uri: http://example.org predicates: @@ -264,7 +253,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: add_request_header_route uri: http://example.org filters: @@ -283,7 +271,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: add_request_parameter_route uri: http://example.org filters: @@ -302,7 +289,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: add_request_header_route uri: http://example.org filters: @@ -321,7 +307,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: hytstrix_route uri: http://example.org filters: @@ -330,6 +315,28 @@ spring: This wraps the remaining filters in a `HystrixCommand` with command name `myCommandName`. +The Hystrix filter takes an optional `fallbackUri` parameter. Currently, only `forward:` schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI. + + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: hytstrix_route + uri: http://example.org + filters: + - name: Hystrix + args: + name: fallbackcmd + fallbackUri: forward:/fallbackcontroller + +This will forward to the `/fallbackcontroller` when the Hystrix fallback is called. +---- + + === PrefixPath GatewayFilter Factory The PrefixPath GatewayFilter Factory takes a single `prefix` parameter. @@ -340,7 +347,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: prefixpath_route uri: http://example.org filters: @@ -359,7 +365,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: preserve_host_route uri: http://example.org filters: @@ -398,7 +403,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: requestratelimiter_route uri: http://example.org filters: @@ -426,7 +430,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: prefixpath_route uri: http://example.org filters: @@ -460,7 +463,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: removerequestheader_route uri: http://example.org filters: @@ -479,7 +481,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: removeresponseheader_route uri: http://example.org filters: @@ -498,7 +499,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: rewritepath_route uri: http://example.org predicates: @@ -545,7 +545,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: setpath_route uri: http://example.org predicates: @@ -566,7 +565,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: setresponseheader_route uri: http://example.org filters: @@ -585,7 +583,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: setstatusstring_route uri: http://example.org filters: @@ -653,7 +650,6 @@ spring: cloud: gateway: routes: - # ===================================== - id: setstatus_route uri: http://example.org filters: 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 5cc29366..86649d7f 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 @@ -327,8 +327,8 @@ public class GatewayAutoConfiguration { @ConditionalOnClass({HystrixObservableCommand.class, RxReactiveStreams.class}) protected static class HystrixConfiguration { @Bean - public HystrixGatewayFilterFactory hystrixGatewayFilterFactory() { - return new HystrixGatewayFilterFactory(); + public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) { + return new HystrixGatewayFilterFactory(dispatcherHandler); } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java index d70aee88..0b60fb88 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.filter.factory; +import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -24,8 +25,11 @@ import java.util.function.Function; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.tuple.Tuple; +import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; @@ -34,6 +38,8 @@ import com.netflix.hystrix.HystrixObservableCommand.Setter; import com.netflix.hystrix.exception.HystrixRuntimeException; import static com.netflix.hystrix.exception.HystrixRuntimeException.FailureType.TIMEOUT; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedQuery; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus; import reactor.core.publisher.Mono; @@ -46,30 +52,58 @@ import rx.Subscription; */ public class HystrixGatewayFilterFactory implements GatewayFilterFactory { + public static final String FALLBACK_URI = "fallbackUri"; + + private final DispatcherHandler dispatcherHandler; + + public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) { + this.dispatcherHandler = dispatcherHandler; + } + @Override public List argNames() { return Arrays.asList(NAME_KEY); } + @Override + public boolean validateArgs() { + return false; + } + @Override public GatewayFilter apply(Tuple args) { //TODO: if no name is supplied, generate one from command id (useful for default filter) - final String commandName = args.getString(NAME_KEY); - return apply(commandName); + String commandName = args.getString(NAME_KEY); + if (args.hasFieldName(FALLBACK_URI)) { + URI fallbackUri = URI.create(args.getString(FALLBACK_URI)); + if (!"forward".equals(fallbackUri.getScheme())) { + throw new IllegalArgumentException("Hystrix Filter currently only supports 'forward' URIs, found "+ fallbackUri); + } + return apply(commandName, fallbackUri); + } + return apply(commandName, null); } public GatewayFilter apply(String commandName) { + return apply(commandName, null); + } + + public GatewayFilter apply(String commandName, URI fallbackUri) { final HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName()); final HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(commandName); final Setter setter = Setter.withGroupKey(groupKey) .andCommandKey(commandKey); - return apply(setter); + return apply(setter, fallbackUri); } public GatewayFilter apply(Setter setter) { + return apply(setter, null); + } + + public GatewayFilter apply(Setter setter, URI fallbackUri) { return (exchange, chain) -> { - RouteHystrixCommand command = new RouteHystrixCommand(setter, exchange, chain); + RouteHystrixCommand command = new RouteHystrixCommand(setter, fallbackUri, exchange, chain); return Mono.create(s -> { Subscription sub = command.toObservable().subscribe(s::success, s::error, s::success); @@ -89,18 +123,43 @@ public class HystrixGatewayFilterFactory implements GatewayFilterFactory { //TODO: replace with HystrixMonoCommand that we write private class RouteHystrixCommand extends HystrixObservableCommand { + + private final URI fallbackUri; private final ServerWebExchange exchange; private final GatewayFilterChain chain; - RouteHystrixCommand(Setter setter, ServerWebExchange exchange, GatewayFilterChain chain) { + RouteHystrixCommand(Setter setter, URI fallbackUri, ServerWebExchange exchange, GatewayFilterChain chain) { super(setter); + this.fallbackUri = fallbackUri; this.exchange = exchange; this.chain = chain; } @Override protected Observable construct() { - return RxReactiveStreams.toObservable(this.chain.filter(this.exchange)); + return RxReactiveStreams.toObservable(this.chain.filter(exchange)); + } + + @Override + protected Observable resumeWithFallback() { + if (this.fallbackUri == null) { + return super.resumeWithFallback(); + } + + //TODO: copied from RouteToRequestUrlFilter + URI uri = exchange.getRequest().getURI(); + boolean encoded = containsEncodedQuery(uri); + URI requestUrl = UriComponentsBuilder.fromUri(uri) + .host(null) + .port(null) + .uri(this.fallbackUri) + .build(encoded) + .toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + + ServerHttpRequest request = this.exchange.getRequest().mutate().uri(requestUrl).build(); + ServerWebExchange mutated = exchange.mutate().request(request).build(); + return RxReactiveStreams.toObservable(HystrixGatewayFilterFactory.this.dispatcherHandler.handle(mutated)); } } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index ea32e5e2..4873f508 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -103,6 +103,14 @@ public class GatewayFilterSpec extends UriSpec { return filter(getBean(HystrixGatewayFilterFactory.class).apply(setter)); } + public GatewayFilterSpec hystrix(String commandName, URI fallbackUri) { + return filter(getBean(HystrixGatewayFilterFactory.class).apply(commandName, fallbackUri)); + } + + public GatewayFilterSpec hystrix(HystrixObservableCommand.Setter setter, URI fallbackUri) { + return filter(getBean(HystrixGatewayFilterFactory.class).apply(setter, fallbackUri)); + } + public GatewayFilterSpec prefixPath(String prefix) { return filter(getBean(PrefixPathGatewayFilterFactory.class).apply(prefix)); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java index 26e7f95c..5c537f4c 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java @@ -17,6 +17,9 @@ package org.springframework.cloud.gateway.filter.factory; +import java.util.Collections; +import java.util.Map; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.SpringBootConfiguration; @@ -24,18 +27,14 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.test.BaseWebClientTests; import org.springframework.context.annotation.Import; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.cloud.gateway.test.TestUtils.assertStatus; - -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -44,40 +43,39 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests { @Test public void hystrixFilterWorks() { - Mono result = webClient.get() - .uri("/get") + testClient.get().uri("/get") .header("Host", "www.hystrixsuccess.org") - .exchange(); - - StepVerifier.create(result) - .consumeNextWith( - response -> { - assertStatus(response, HttpStatus.OK); - HttpHeaders httpHeaders = response.headers().asHttpHeaders(); - assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) - .isEqualTo("hystrix_success_test"); - }) - .expectComplete() - .verify(DURATION); + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "hystrix_success_test"); } @Test public void hystrixFilterTimesout() { - Mono result = webClient.get() - .uri("/delay/3") + testClient.get().uri("/delay/3") .header("Host", "www.hystrixfailure.org") - .exchange(); + .exchange() + .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT); + } - StepVerifier.create(result) - .consumeNextWith( - response -> assertStatus(response, HttpStatus.GATEWAY_TIMEOUT)) - .expectComplete() - .verify(DURATION); + @Test + public void hystrixFilterFallback() { + testClient.get().uri("/delay/3?a=b") + .header("Host", "www.hystrixfallback.org") + .exchange() + .expectStatus().isOk() + .expectBody().json("{\"from\":\"fallbackcontroller\"}"); } @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class) - public static class TestConfig { } + @RestController + public static class TestConfig { + @RequestMapping("/fallbackcontroller") + public Map fallbackcontroller(@RequestParam("a") String a) { + return Collections.singletonMap("from", "fallbackcontroller"); + } + } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java index e4fcfc6e..f3c25b87 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java @@ -57,9 +57,7 @@ public class ForwardTests { @Test public void forwardWorks() { - this.client - .get() - .uri("/localcontroller") + this.client.get().uri("/localcontroller") .header(HttpHeaders.HOST, "www.forward.org") .exchange() .expectStatus().isOk() diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 41654845..d477315c 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -60,7 +60,7 @@ spring: # ===================================== - id: forward_test - uri: forward:///localcontroller + uri: forward:/localcontroller predicates: - Host=**.forward.org @@ -72,6 +72,17 @@ spring: filters: - Hystrix=failcmd + # ===================================== + - id: hystrix_fallback_test + uri: ${test.uri} + predicates: + - Host=**.hystrixfallback.org + filters: + - name: Hystrix + args: + name: fallbackcmd + fallbackUri: forward:/fallbackcontroller + # ===================================== - id: hystrix_success_test uri: ${test.uri}