Add exception info for hystrix fallback (#657)
* Add Hystrix execution exception attribute to fallback filter. Add FallbackHeaders filter to add headers with exception info for external handling. Fixes gh-601. * Add tests and docs. * Reformat imports. * Add _ATTR suffix to hystrix exception attribute and reference it in docs.
This commit is contained in:
committed by
GitHub
parent
e2b19d664a
commit
ee1e72660a
@@ -356,6 +356,7 @@ spring:
|
||||
|
||||
This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests.
|
||||
|
||||
[[hystrix]]
|
||||
=== Hystrix GatewayFilter Factory
|
||||
https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern].
|
||||
The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures.
|
||||
@@ -402,6 +403,42 @@ spring:
|
||||
----
|
||||
This will forward to the `/incaseoffailureusethis` URI when the Hystrix fallback is called. Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing via the `lb` prefix on the destination URI.
|
||||
|
||||
The primary scenario is to use the `fallbackUri` to an internal controller or handler within the gateway app.
|
||||
However, it is also possible to reroute the request to a controller or handler in an external application, like so:
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: ingredients
|
||||
uri: lb://ingredients
|
||||
predicates:
|
||||
- Path=//ingredients/**
|
||||
filters:
|
||||
- name: Hystrix
|
||||
args:
|
||||
name: fetchIngredients
|
||||
fallbackUri: forward:/fallback
|
||||
- id: ingredients-fallback
|
||||
uri: http://localhost:9994
|
||||
predicates:
|
||||
- Path=/fallback
|
||||
----
|
||||
|
||||
In this example, there is no `fallback` endpoint or handler in the gateway application, however, there is one in another
|
||||
app, registered under `http://localhost:9994`.
|
||||
|
||||
In case of the request being forwarded to fallback, the Hystrix Gateway filter also provides the `Throwable` that has
|
||||
caused it. It's added to the `ServerWebExchange` as the
|
||||
`ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR` attribute that can be used when
|
||||
handling the fallback within the gateway app.
|
||||
|
||||
For the external controller/ handler scenario, headers can be added with exception details. You can find more information
|
||||
on it in the <<fallback-headers, FallbackHeaders GatewayFilter Factory section>>.
|
||||
|
||||
Hystrix settings (such as timeouts) can be configured with global defaults or on a route by route basis using application properties as explained on the https://github.com/Netflix/Hystrix/wiki/Configuration[Hystrix wiki].
|
||||
|
||||
To set a 5 second timeout for the example route above, the following configuration would be used:
|
||||
@@ -410,6 +447,52 @@ To set a 5 second timeout for the example route above, the following configurati
|
||||
[source,yaml]
|
||||
hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000
|
||||
|
||||
[[fallback-headers]]
|
||||
=== FallbackHeaders GatewayFilter Factory
|
||||
|
||||
The `FallbackHeaders` factory allows you to add Hystrix execution exception details in headers of a request forwarded to
|
||||
a `fallbackUri` in an external application, like in the following scenario:
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: ingredients
|
||||
uri: lb://ingredients
|
||||
predicates:
|
||||
- Path=//ingredients/**
|
||||
filters:
|
||||
- name: Hystrix
|
||||
args:
|
||||
name: fetchIngredients
|
||||
fallbackUri: forward:/fallback
|
||||
- id: ingredients-fallback
|
||||
uri: http://localhost:9994
|
||||
predicates:
|
||||
- Path=/fallback
|
||||
filters:
|
||||
- name: FallbackHeaders
|
||||
args:
|
||||
executionExceptionTypeHeaderName: Test-Header
|
||||
----
|
||||
|
||||
In this example, after an execution exception occurs while running the `HystrixCommand`, the request will be forwarde to
|
||||
the `fallback` endpoint or handler in an app running on `localhost:9994`. The headers with the exception type, message
|
||||
and -if available- root cause exception type and message will be added to that request by the `FallbackHeaders` filter.
|
||||
|
||||
The names of the headers can be overwritten in the config by setting the values of the arguments listed below, along with
|
||||
their default values:
|
||||
|
||||
* `executionExceptionTypeHeaderName` (`"Execution-Exception-Type"`)
|
||||
* `executionExceptionMessageHeaderName` (`"Execution-Exception-Message"`)
|
||||
* `rootCauseExceptionTypeHeaderName` (`"Root-Cause-Exception-Type"`)
|
||||
* `rootCauseExceptionMessageHeaderName` (`"Root-Cause-Exception-Message"`)
|
||||
|
||||
You can find more information on how Hystrix works with Gateway in the <<hystrix, Hystrix GatewayFilter Factory section>>.
|
||||
|
||||
=== PrefixPath GatewayFilter Factory
|
||||
The PrefixPath GatewayFilter Factory takes a single `prefix` parameter.
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.resources.ConnectionProvider;
|
||||
import reactor.netty.tcp.ProxyProvider;
|
||||
import reactor.netty.tcp.SslProvider;
|
||||
import rx.RxReactiveStreams;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -58,6 +57,7 @@ import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
|
||||
@@ -501,6 +501,11 @@ public class GatewayAutoConfiguration {
|
||||
public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(ObjectProvider<DispatcherHandler> dispatcherHandler) {
|
||||
return new HystrixGatewayFilterFactory(dispatcherHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
|
||||
return new FallbackHeadersGatewayFilterFactory();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.factory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.apache.commons.lang.exception.ExceptionUtils.getRootCause;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR;
|
||||
|
||||
/**
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class FallbackHeadersGatewayFilterFactory extends AbstractGatewayFilterFactory<FallbackHeadersGatewayFilterFactory.Config> {
|
||||
|
||||
public FallbackHeadersGatewayFilterFactory() {
|
||||
super(Config.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return singletonList(NAME_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
return (exchange, chain) -> {
|
||||
ServerWebExchange filteredExchange = ofNullable((Throwable) exchange
|
||||
.getAttribute(HYSTRIX_EXECUTION_EXCEPTION_ATTR))
|
||||
.map(executionException -> {
|
||||
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
|
||||
requestBuilder.header(config.executionExceptionTypeHeaderName, executionException.getClass().getName());
|
||||
requestBuilder.header(config.executionExceptionMessageHeaderName, executionException.getMessage());
|
||||
ofNullable(getRootCause(executionException)).ifPresent(rootCause -> {
|
||||
requestBuilder.header(config.rootCauseExceptionTypeHeaderName, rootCause.getClass().getName());
|
||||
requestBuilder.header(config.rootCauseExceptionMessageHeaderName, rootCause.getMessage());
|
||||
});
|
||||
return exchange.mutate().request(requestBuilder.build()).build();
|
||||
}).orElse(exchange);
|
||||
return chain.filter(filteredExchange);
|
||||
};
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
private static final String EXECUTION_EXCEPTION_TYPE = "Execution-Exception-Type";
|
||||
private static final String EXECUTION_EXCEPTION_MESSAGE = "Execution-Exception-Message";
|
||||
private static final String ROOT_CAUSE_EXCEPTION_TYPE = "Root-Cause-Exception-Type";
|
||||
private static final String ROOT_CAUSE_EXCEPTION_MESSAGE = "Root-Cause-Exception-Message";
|
||||
|
||||
private String executionExceptionTypeHeaderName = EXECUTION_EXCEPTION_TYPE;
|
||||
private String executionExceptionMessageHeaderName = EXECUTION_EXCEPTION_MESSAGE;
|
||||
private String rootCauseExceptionTypeHeaderName = ROOT_CAUSE_EXCEPTION_TYPE;
|
||||
private String rootCauseExceptionMessageHeaderName = ROOT_CAUSE_EXCEPTION_MESSAGE;
|
||||
|
||||
public String getExecutionExceptionTypeHeaderName() {
|
||||
return executionExceptionTypeHeaderName;
|
||||
}
|
||||
|
||||
public void setExecutionExceptionTypeHeaderName(String executionExceptionTypeHeaderName) {
|
||||
this.executionExceptionTypeHeaderName = executionExceptionTypeHeaderName;
|
||||
}
|
||||
|
||||
public String getExecutionExceptionMessageHeaderName() {
|
||||
return executionExceptionMessageHeaderName;
|
||||
}
|
||||
|
||||
public void setExecutionExceptionMessageHeaderName(String executionExceptionMessageHeaderName) {
|
||||
this.executionExceptionMessageHeaderName = executionExceptionMessageHeaderName;
|
||||
}
|
||||
|
||||
public String getRootCauseExceptionTypeHeaderName() {
|
||||
return rootCauseExceptionTypeHeaderName;
|
||||
}
|
||||
|
||||
public void setRootCauseExceptionTypeHeaderName(String rootCauseExceptionTypeHeaderName) {
|
||||
this.rootCauseExceptionTypeHeaderName = rootCauseExceptionTypeHeaderName;
|
||||
}
|
||||
|
||||
public String getCauseExceptionMessageHeaderName() {
|
||||
return rootCauseExceptionMessageHeaderName;
|
||||
}
|
||||
|
||||
public void setCauseExceptionMessageHeaderName(String causeExceptionMessageHeaderName) {
|
||||
this.rootCauseExceptionMessageHeaderName = causeExceptionMessageHeaderName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -47,16 +46,20 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static com.netflix.hystrix.exception.HystrixRuntimeException.FailureType.TIMEOUT;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
|
||||
|
||||
/**
|
||||
* Depends on `spring-cloud-starter-netflix-hystrix`, {@see http://cloud.spring.io/spring-cloud-netflix/}
|
||||
* @author Spencer Gibb
|
||||
* @author Michele Mancioppi
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<HystrixGatewayFilterFactory.Config> {
|
||||
|
||||
private final ObjectProvider<DispatcherHandler> dispatcherHandler;
|
||||
|
||||
public HystrixGatewayFilterFactory(ObjectProvider<DispatcherHandler> dispatcherHandler) {
|
||||
@@ -66,7 +69,7 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<Hy
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList(NAME_KEY);
|
||||
return singletonList(NAME_KEY);
|
||||
}
|
||||
|
||||
public GatewayFilter apply(String routeId, Consumer<Config> consumer) {
|
||||
@@ -162,12 +165,19 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<Hy
|
||||
.build(encoded)
|
||||
.toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
addExceptionDetails();
|
||||
|
||||
ServerHttpRequest request = this.exchange.getRequest().mutate().uri(requestUrl).build();
|
||||
ServerWebExchange mutated = exchange.mutate().request(request).build();
|
||||
DispatcherHandler dispatcherHandler = HystrixGatewayFilterFactory.this.dispatcherHandler.getIfAvailable();
|
||||
return RxReactiveStreams.toObservable(dispatcherHandler.handle(mutated));
|
||||
}
|
||||
|
||||
private void addExceptionDetails() {
|
||||
Throwable executionException = getExecutionException();
|
||||
ofNullable(executionException)
|
||||
.ifPresent(exception -> exchange.getAttributes().put(HYSTRIX_EXECUTION_EXCEPTION_ATTR, exception));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUri
|
||||
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory;
|
||||
@@ -567,4 +568,43 @@ public class GatewayFilterSpec extends UriSpec {
|
||||
return filter(getBean(RequestSizeGatewayFilterFactory.class).apply(c -> c.setMaxSize(size)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hystrix execution exception headers to fallback request.
|
||||
* Depends on @{code org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the classpath,
|
||||
* {@see http://cloud.spring.io/spring-cloud-netflix/}
|
||||
*
|
||||
* @param config a {@link FallbackHeadersGatewayFilterFactory.Config} which provides the header names.
|
||||
* If header names arguments are not provided, default values are used.
|
||||
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
|
||||
*/
|
||||
public GatewayFilterSpec fallbackHeaders(FallbackHeadersGatewayFilterFactory.Config config) {
|
||||
FallbackHeadersGatewayFilterFactory factory = getFallbackHeadersGatewayFilterFactory();
|
||||
return filter(factory.apply(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hystrix execution exception headers to fallback request.
|
||||
* Depends on @{code org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the classpath,
|
||||
* {@see http://cloud.spring.io/spring-cloud-netflix/}
|
||||
*
|
||||
* @param configConsumer a {@link Consumer} which can be used to set up the names of the headers in the config.
|
||||
* If header names arguments are not provided, default values are used.
|
||||
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
|
||||
*/
|
||||
public GatewayFilterSpec fallbackHeaders(Consumer<FallbackHeadersGatewayFilterFactory.Config> configConsumer) {
|
||||
FallbackHeadersGatewayFilterFactory factory = getFallbackHeadersGatewayFilterFactory();
|
||||
return filter(factory.apply(configConsumer));
|
||||
}
|
||||
|
||||
private FallbackHeadersGatewayFilterFactory getFallbackHeadersGatewayFilterFactory() {
|
||||
FallbackHeadersGatewayFilterFactory factory;
|
||||
try {
|
||||
factory = getBean(FallbackHeadersGatewayFilterFactory.class);
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
throw new NoSuchBeanDefinitionException(FallbackHeadersGatewayFilterFactory.class,
|
||||
"This is probably because Hystrix is missing from the classpath, which can be resolved by adding dependency on 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'");
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public class ServerWebExchangeUtils {
|
||||
public static final String GATEWAY_PREDICATE_ROUTE_ATTR = qualify("gatewayPredicateRouteAttr");
|
||||
public static final String WEIGHT_ATTR = qualify("routeWeight");
|
||||
public static final String ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR = "original_response_content_type";
|
||||
public static final String HYSTRIX_EXECUTION_EXCEPTION_ATTR = qualify("hystrixExecutionException");
|
||||
|
||||
/**
|
||||
* Used when a routing filter has been successfully call. Allows users to write custom
|
||||
|
||||
@@ -17,40 +17,26 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import com.netflix.loadbalancer.ServerList;
|
||||
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.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.cloud.netflix.ribbon.RibbonClient;
|
||||
import org.springframework.cloud.netflix.ribbon.StaticServerList;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.core.StringContains.containsString;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.cloud.gateway.filter.factory.ExceptionFallbackHandler.RETRIEVED_EXCEPTION;
|
||||
import static org.springframework.http.MediaType.TEXT_HTML;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "debug=true")
|
||||
@ContextConfiguration(classes = HystrixTestConfig.class)
|
||||
@DirtiesContext
|
||||
public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
|
||||
@@ -97,6 +83,15 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
.expectBody().json("{\"from\":\"fallbackcontroller\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hystrixFilterExceptionFallback() {
|
||||
testClient.get().uri("/delay/3")
|
||||
.header("Host", "www.hystrixexceptionfallback.org")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().value(RETRIEVED_EXCEPTION, containsString("HystrixTimeoutException"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hystrixFilterWorksJavaDsl() {
|
||||
testClient.get().uri("/get")
|
||||
@@ -139,58 +134,4 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
"Cannot find the expected error status report in the response");
|
||||
});
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
@RestController
|
||||
@RibbonClient(name = "badservice", configuration = TestBadRibbonConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
private String uri;
|
||||
|
||||
@RequestMapping("/fallbackcontroller")
|
||||
public Map<String, String> fallbackcontroller(@RequestParam("a") String a) {
|
||||
return Collections.singletonMap("from", "fallbackcontroller");
|
||||
}
|
||||
|
||||
@RequestMapping("/fallbackcontroller2")
|
||||
public Map<String, String> fallbackcontroller2() {
|
||||
return Collections.singletonMap("from", "fallbackcontroller2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouteLocator hystrixRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("hystrix_java", r -> r.host("**.hystrixjava.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> config.setFallbackUri("forward:/fallbackcontroller2")))
|
||||
.uri(uri))
|
||||
.route("hystrix_connection_failure", r -> r.host("**.hystrixconnectfail.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> {}))
|
||||
.uri("lb:badservice"))
|
||||
/*
|
||||
* This is a route encapsulated in a hystrix command that is ready to wait
|
||||
* for a response far longer than the underpinning WebClient would.
|
||||
*/
|
||||
.route("hystrix_response_stall", r -> r.host("**.hystrixresponsestall.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> config.setName("stalling-command")))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class TestBadRibbonConfig {
|
||||
|
||||
@LocalServerPort
|
||||
protected int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServerList<Server> ribbonServerList() {
|
||||
return new StaticServerList<>(new Server("https", "localhost", this.port));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.factory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import com.netflix.loadbalancer.ServerList;
|
||||
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.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.cloud.netflix.ribbon.RibbonClient;
|
||||
import org.springframework.cloud.netflix.ribbon.StaticServerList;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(BaseWebClientTests.DefaultTestConfig.class)
|
||||
@RestController
|
||||
@RibbonClient(name = "badservice", configuration = TestBadRibbonConfig.class)
|
||||
public class HystrixTestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
private String uri;
|
||||
|
||||
@RequestMapping("/fallbackcontroller")
|
||||
public Map<String, String> fallbackcontroller(@RequestParam("a") String a) {
|
||||
return Collections.singletonMap("from", "fallbackcontroller");
|
||||
}
|
||||
|
||||
@RequestMapping("/fallbackcontroller2")
|
||||
public Map<String, String> fallbackcontroller2() {
|
||||
return Collections.singletonMap("from", "fallbackcontroller2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouteLocator hystrixRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("hystrix_java", r -> r.host("**.hystrixjava.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> config.setFallbackUri("forward:/fallbackcontroller2")))
|
||||
.uri(uri))
|
||||
.route("hystrix_connection_failure", r -> r.host("**.hystrixconnectfail.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> {
|
||||
}))
|
||||
.uri("lb:badservice"))
|
||||
/*
|
||||
* This is a route encapsulated in a hystrix command that is ready to wait
|
||||
* for a response far longer than the underpinning WebClient would.
|
||||
*/
|
||||
.route("hystrix_response_stall", r -> r.host("**.hystrixresponsestall.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.hystrix(config -> config.setName("stalling-command")))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExceptionFallbackHandler exceptionFallbackHandler() {
|
||||
return new ExceptionFallbackHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> routerFunction(ExceptionFallbackHandler exceptionFallbackHandler) {
|
||||
return route(GET("/exceptionFallback"), exceptionFallbackHandler::retrieveExceptionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
class ExceptionFallbackHandler {
|
||||
|
||||
static final String RETRIEVED_EXCEPTION = "Retrieved-Exception";
|
||||
|
||||
Mono<ServerResponse> retrieveExceptionInfo(ServerRequest serverRequest) {
|
||||
String exceptionName = serverRequest.attribute(HYSTRIX_EXECUTION_EXCEPTION_ATTR)
|
||||
.map(exception -> exception.getClass().getName())
|
||||
.orElse("");
|
||||
return ServerResponse.ok().header(RETRIEVED_EXCEPTION, exceptionName)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
class TestBadRibbonConfig {
|
||||
|
||||
@LocalServerPort
|
||||
protected int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServerList<Server> ribbonServerList() {
|
||||
return new StaticServerList<>(new Server("https", "localhost", this.port));
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public class BaseWebClientTests {
|
||||
@RibbonClient(name = "myservice", configuration = TestRibbonConfig.class)
|
||||
})
|
||||
@Import(PermitAllSecurityConfiguration.class)
|
||||
protected static class DefaultTestConfig {
|
||||
public static class DefaultTestConfig {
|
||||
private static final Log log = LogFactory.getLog(DefaultTestConfig.class);
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -90,6 +90,17 @@ spring:
|
||||
name: fallbackcmd
|
||||
fallbackUri: forward:/fallbackcontroller
|
||||
|
||||
# =====================================
|
||||
- id: hystrix_exception_fallback_test
|
||||
uri: ${test.uri}
|
||||
predicates:
|
||||
- Host=**.hystrixexceptionfallback.org
|
||||
filters:
|
||||
- name: Hystrix
|
||||
args:
|
||||
name: fallbackcmd
|
||||
fallbackUri: forward:/exceptionFallback
|
||||
|
||||
# =====================================
|
||||
- id: hystrix_success_test
|
||||
uri: ${test.uri}
|
||||
|
||||
Reference in New Issue
Block a user