Skip to content

Commit 0276702

Browse files
Treat corner cases of Hystrix exception handling
* Treat HttpClient timeouts the same as Hystrix timeouts * Propagate ResponseStatusException and throwables with @ResponseStatus when cause for COMMAND_EXECUTION failure Fix #554
1 parent b42c45f commit 0276702

File tree

4 files changed

+52
-7
lines changed

4 files changed

+52
-7
lines changed

spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,24 @@
3636
import org.springframework.beans.factory.ObjectProvider;
3737
import org.springframework.cloud.gateway.filter.GatewayFilter;
3838
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
39-
import org.springframework.http.HttpStatus;
39+
import org.springframework.cloud.gateway.support.TimeoutException;
40+
import org.springframework.core.annotation.AnnotatedElementUtils;
4041
import org.springframework.http.server.reactive.ServerHttpRequest;
4142
import org.springframework.util.Assert;
4243
import org.springframework.util.StringUtils;
44+
import org.springframework.web.bind.annotation.ResponseStatus;
4345
import org.springframework.web.reactive.DispatcherHandler;
4446
import org.springframework.web.server.ResponseStatusException;
4547
import org.springframework.web.server.ServerWebExchange;
4648
import org.springframework.web.util.UriComponentsBuilder;
4749

48-
import static com.netflix.hystrix.exception.HystrixRuntimeException.FailureType.TIMEOUT;
4950
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
5051
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
5152

5253
/**
5354
* Depends on `spring-cloud-starter-netflix-hystrix`, {@see http://cloud.spring.io/spring-cloud-netflix/}
5455
* @author Spencer Gibb
56+
* @author Michele Mancioppi
5557
*/
5658
public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<HystrixGatewayFilterFactory.Config> {
5759

@@ -101,8 +103,24 @@ public GatewayFilter apply(Config config) {
101103
}).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> {
102104
if (throwable instanceof HystrixRuntimeException) {
103105
HystrixRuntimeException e = (HystrixRuntimeException) throwable;
104-
if (e.getFailureType() == TIMEOUT) {
105-
return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT));
106+
HystrixRuntimeException.FailureType failureType = e.getFailureType();
107+
108+
switch (failureType) {
109+
case TIMEOUT:
110+
return Mono.error(new TimeoutException());
111+
case COMMAND_EXCEPTION: {
112+
Throwable cause = e.getCause();
113+
114+
/*
115+
* We forsake here the null check for cause as HystrixRuntimeException will
116+
* always have a cause if the failure type is COMMAND_EXCEPTION.
117+
*/
118+
if (cause instanceof ResponseStatusException || AnnotatedElementUtils
119+
.findMergedAnnotation(cause.getClass(), ResponseStatus.class) != null) {
120+
return Mono.error(cause);
121+
}
122+
}
123+
default: break;
106124
}
107125
}
108126
return Mono.error(throwable);

spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
package org.springframework.cloud.gateway.support;
1919

20+
import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT;
21+
22+
import org.springframework.web.bind.annotation.ResponseStatus;
23+
24+
@ResponseStatus(value = GATEWAY_TIMEOUT, reason = "Response took longer than configured timeout")
2025
public class TimeoutException extends Exception {
2126

2227
public TimeoutException() {

spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.boot.test.context.SpringBootTest;
2828
import org.springframework.cloud.gateway.test.BaseWebClientTests;
2929
import org.springframework.context.annotation.Import;
30+
import org.springframework.http.HttpStatus;
3031
import org.springframework.test.annotation.DirtiesContext;
3132
import org.springframework.test.context.junit4.SpringRunner;
3233

@@ -44,11 +45,11 @@ public void responseTimeoutWorks() {
4445
testClient.get()
4546
.uri("/delay/5")
4647
.exchange()
47-
.expectStatus().is5xxServerError()
48+
.expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT)
4849
.expectBody(Map.class)
4950
.consumeWith(result -> {
5051
Map body = result.getResponseBody();
51-
assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S");
52+
assertThat(body).containsEntry("message", "Response took longer than configured timeout");
5253
});
5354
}
5455

spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ public void hystrixFilterTimesout() {
7474
.isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value()));
7575
}
7676

77+
/*
78+
* Tests that timeouts bubbling from the underpinning WebClient are treated the same as
79+
* Hystrix timeouts in terms of outside response. (Internally, timeouts from the WebClient
80+
* are seen as command failures and trigger the opening of circuit breakers the same way
81+
* timeouts do; it may be confusing in terms of the Hystrix metrics though)
82+
*/
83+
@Test
84+
public void hystrixTimeoutFromWebClient() {
85+
testClient.get().uri("/delay/10")
86+
.header("Host", "www.hystrixresponsestall.org")
87+
.exchange()
88+
.expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT);
89+
}
90+
7791
@Test
7892
public void hystrixFilterFallback() {
7993
testClient.get().uri("/delay/3?a=b")
@@ -157,11 +171,18 @@ public RouteLocator hystrixRouteLocator(RouteLocatorBuilder builder) {
157171
.filters(f -> f.prefixPath("/httpbin")
158172
.hystrix(config -> {}))
159173
.uri("lb:badservice"))
174+
/*
175+
* This is a route encapsulated in a hystrix command that is ready to wait
176+
* for a response far longer than the underpinning WebClient would.
177+
*/
178+
.route("hystrix_response_stall", r -> r.host("**.hystrixresponsestall.org")
179+
.filters(f -> f.prefixPath("/httpbin")
180+
.hystrix(config -> config.setName("stalling-command")))
181+
.uri(uri))
160182
.build();
161183
}
162184
}
163185

164-
165186
protected static class TestBadRibbonConfig {
166187

167188
@LocalServerPort

0 commit comments

Comments
 (0)