diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 7be2e7eb..130aa8b1 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -2221,6 +2221,19 @@ import static org.springframework.cloud.gateway.support.RouteMetadataUtils.RESPO } ---- +A per-route `response-timeout` with a negative value will disable the global `response-timeout` value. + +---- + - id: per_route_timeouts + uri: https://example.org + predicates: + - name: Path + args: + pattern: /delay/{timeout} + metadata: + response-timeout: -1 +---- + === Fluent Java Routes API To allow for simple configuration in Java, the `RouteLocatorBuilder` bean includes a fluent API. diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index 387e1a81..d0f12acb 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java @@ -71,13 +71,13 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.s */ public class NettyRoutingFilter implements GlobalFilter, Ordered { - private static final Log log = LogFactory.getLog(NettyRoutingFilter.class); - /** * The order of the NettyRoutingFilter. See {@link Ordered#LOWEST_PRECEDENCE}. */ public static final int ORDER = Ordered.LOWEST_PRECEDENCE; + private static final Log log = LogFactory.getLog(NettyRoutingFilter.class); + private final HttpClient httpClient; private final ObjectProvider> headersFiltersProvider; @@ -261,16 +261,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { private Duration getResponseTimeout(Route route) { Object responseTimeoutAttr = route.getMetadata().get(RESPONSE_TIMEOUT_ATTR); - Long responseTimeout = null; - if (responseTimeoutAttr != null) { - if (responseTimeoutAttr instanceof Number) { - responseTimeout = ((Number) responseTimeoutAttr).longValue(); + if (responseTimeoutAttr != null && responseTimeoutAttr instanceof Number) { + Long routeResponseTimeout = ((Number) responseTimeoutAttr).longValue(); + if (routeResponseTimeout >= 0) { + return Duration.ofMillis(routeResponseTimeout); } else { - responseTimeout = Long.valueOf(responseTimeoutAttr.toString()); + return null; } } - return responseTimeout != null ? Duration.ofMillis(responseTimeout) : properties.getResponseTimeout(); + return properties.getResponseTimeout(); } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterCompatibleTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterCompatibleTests.java index d2c65518..220cd869 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterCompatibleTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterCompatibleTests.java @@ -26,6 +26,7 @@ import org.springframework.cloud.gateway.test.BaseWebClientTests; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; @@ -39,17 +40,9 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen * * @author echooymxq **/ -@SpringBootTest(properties = { "spring.cloud.gateway.routes[0].id=route_connect_timeout", - "spring.cloud.gateway.routes[0].uri=http://localhost:32167", - "spring.cloud.gateway.routes[0].predicates[0].name=Path", - "spring.cloud.gateway.routes[0].predicates[0].args[pattern]=/connect/delay/{timeout}", - "spring.cloud.gateway.routes[0].metadata[connect-timeout]=5", - "spring.cloud.gateway.routes[1].id=route_response_timeout", - "spring.cloud.gateway.routes[1].uri=lb://testservice", "spring.cloud.gateway.routes[1].predicates[0].name=Path", - "spring.cloud.gateway.routes[1].predicates[0].args[pattern]=/route/delay/{timeout}", - "spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1", - "spring.cloud.gateway.routes[1].metadata.response-timeout=1000" }, webEnvironment = RANDOM_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext +@ActiveProfiles("netty-routing-filter") class NettyRoutingFilterCompatibleTests extends BaseWebClientTests { @Test diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java index f86a0219..b1aec30e 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java @@ -32,17 +32,20 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.data.Offset.offset; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -@SpringBootTest(properties = "spring.cloud.gateway.httpclient.response-timeout=3s", webEnvironment = RANDOM_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext +@ActiveProfiles("netty-routing-filter") public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests { @Autowired @@ -109,6 +112,21 @@ public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests { .jsonPath("$.message").isEqualTo("Response took longer than timeout: PT1S"); } + @Test + public void shouldNotApplyResponseTimeoutPerRouteWhenNegativeValue() { + assertThatThrownBy(() -> { + testClient.get().uri("/disabledRoute/delay/10").exchange(); + }).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Timeout on blocking read for 5000000000 NANOSECONDS"); + } + + @Test + public void shouldApplyGlobalResponseTimeoutForInvalidRouteTimeoutValue() { + testClient.get().uri("/invalidRoute/delay/5").exchange().expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT) + .expectBody().jsonPath("$.status").isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())) + .jsonPath("$.message").isEqualTo("Response took longer than timeout: PT3S"); + } + @Test public void shouldNotApplyPerRouteTimeoutWhenItIsNotConfigured() { testClient.get().uri("/delay/2").exchange().expectStatus().isEqualTo(HttpStatus.OK); diff --git a/spring-cloud-gateway-server/src/test/resources/application-netty-routing-filter.yml b/spring-cloud-gateway-server/src/test/resources/application-netty-routing-filter.yml new file mode 100644 index 00000000..a8c2fdf7 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/resources/application-netty-routing-filter.yml @@ -0,0 +1,73 @@ +test: + hostport: httpbin.org:80 + uri: lb://testservice + +server: + error: + include-message: always + +spring: + profiles: + group: + - logging + + cloud: + gateway: + routes: + # ===================================== + - id: per_route_connect_timeout + uri: http://localhost:32167 + predicates: + - name: Path + args: + pattern: /connect/delay/{timeout} + metadata: + connect-timeout: 5 + + # ===================================== + - id: per_route_response_timeout + uri: ${test.uri} + predicates: + - name: Path + args: + pattern: /route/delay/{timeout} + filters: + - StripPrefix=1 + metadata: + response-timeout: 1000 + + # ===================================== + - id: per_route_response_timeout_disabled + uri: ${test.uri} + predicates: + - name: Path + args: + pattern: /disabledRoute/delay/{timeout} + filters: + - StripPrefix=1 + metadata: + response-timeout: -1 + + # ===================================== + - id: per_route_response_timeout_invalid + uri: ${test.uri} + predicates: + - name: Path + args: + pattern: /invalidRoute/delay/{timeout} + filters: + - StripPrefix=1 + metadata: + response-timeout: notANumber + + # ===================================== + # should be last and not follow alphabetical order + - id: default_path_to_httpbin + uri: ${test.uri} + order: 10000 + predicates: + - name: Path + args: + pattern: /** + httpclient: + response-timeout: 3s diff --git a/spring-cloud-gateway-server/src/test/resources/application.yml b/spring-cloud-gateway-server/src/test/resources/application.yml index 42dfee80..e4ede706 100644 --- a/spring-cloud-gateway-server/src/test/resources/application.yml +++ b/spring-cloud-gateway-server/src/test/resources/application.yml @@ -262,28 +262,6 @@ spring: filters: - SetPath=/anything/multi{num} - # ===================================== - - id: per_route_connect_timeout - uri: http://localhost:32167 - predicates: - - name: Path - args: - pattern: /connect/delay/{timeout} - metadata: - connect-timeout: 5 - - # ===================================== - - id: per_route_response_timeout - uri: ${test.uri} - predicates: - - name: Path - args: - pattern: /route/delay/{timeout} - filters: - - StripPrefix=1 - metadata: - response-timeout: 1000 - # ===================================== - id: redirect_to_test uri: ${test.uri}