diff --git a/pom.xml b/pom.xml
index 06beb9d8..74e0848b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
org.springframework.cloud
spring-cloud-build
- 2.0.3.RELEASE
+ 2.0.4.BUILD-SNAPSHOT
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 0ee16bc5..cb4bfe1d 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
@@ -25,6 +25,7 @@ import com.netflix.hystrix.HystrixObservableCommand;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.client.HttpClientOptions;
import reactor.ipc.netty.options.ClientProxyOptions;
@@ -122,11 +123,14 @@ import org.springframework.core.env.Environment;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
+import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import org.springframework.web.reactive.socket.server.WebSocketService;
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilterChain;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED;
@@ -243,6 +247,19 @@ public class GatewayAutoConfiguration {
}
}
+ //TODO: remove when not needed anymore
+ // either https://jira.spring.io/browse/SPR-17291 or
+ // https://github.com/spring-projects/spring-boot/issues/14520 needs to be fixed
+ @Bean
+ public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
+ return new HiddenHttpMethodFilter() {
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ return chain.filter(exchange);
+ }
+ };
+ }
+
@Bean
public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) {
return new RouteLocatorBuilder(context);
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java
index 2879c5ab..7c72e644 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java
@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.discovery;
import java.net.URI;
import java.util.Map;
+import java.util.function.Predicate;
import reactor.core.publisher.Flux;
@@ -35,7 +36,6 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.StringUtils;
/**
- * TODO: developer configuration, in zuul, this was opt out, should be opt in
* TODO: change to RouteLocator? use java dsl
* @author Spencer Gibb
*/
@@ -44,6 +44,7 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
private final DiscoveryClient discoveryClient;
private final DiscoveryLocatorProperties properties;
private final String routeIdPrefix;
+ private final SimpleEvaluationContext evalCtxt;
public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
this.discoveryClient = discoveryClient;
@@ -53,30 +54,37 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
} else {
this.routeIdPrefix = this.discoveryClient.getClass().getSimpleName() + "_";
}
+ evalCtxt = SimpleEvaluationContext
+ .forReadOnlyDataBinding()
+ .withInstanceMethods()
+ .build();
}
@Override
public Flux getRouteDefinitions() {
- SimpleEvaluationContext evalCtxt = SimpleEvaluationContext
- .forReadOnlyDataBinding()
- .withInstanceMethods()
- .build();
SpelExpressionParser parser = new SpelExpressionParser();
Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
+ Predicate includePredicate;
+ if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) {
+ includePredicate = instance -> true;
+ } else {
+ includePredicate = instance -> {
+ Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
+ if (include == null) {
+ return false;
+ }
+ return include;
+ };
+ }
+
return Flux.fromIterable(discoveryClient.getServices())
.map(discoveryClient::getInstances)
.filter(instances -> !instances.isEmpty())
.map(instances -> instances.get(0))
- .filter(instance -> {
- Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
- if (include == null) {
- return false;
- }
- return include;
- })
+ .filter(includePredicate)
.map(instance -> {
String serviceId = instance.getServiceId();
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 27be9346..49ff8b26 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
@@ -19,9 +19,12 @@ package org.springframework.cloud.gateway.filter;
import java.net.URI;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.timeout.ReadTimeoutException;
+import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.NettyPipeline;
import reactor.ipc.netty.http.client.HttpClient;
@@ -111,17 +114,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
proxyRequest.header(HttpHeaders.HOST, host);
}
+ if (properties.getResponseTimeout() != null) {
+ proxyRequest.context(ctx -> ctx.addHandlerFirst(
+ new ReadTimeoutHandler(properties.getResponseTimeout().toMillis(), TimeUnit.MILLISECONDS)));
+ }
+
return proxyRequest.sendHeaders() //I shouldn't need this
.send(request.getBody().map(dataBuffer ->
((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
@@ -150,6 +152,10 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
// Defer committing the response until all route filters have run
// Put client response as ServerWebExchange attribute and write response later NettyWriteResponseFilter
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
- }).then(chain.filter(exchange));
+ })
+ .onErrorMap(t -> properties.getResponseTimeout() != null && t instanceof ReadTimeoutException,
+ t -> new TimeoutException("Response took longer than timeout: " +
+ properties.getResponseTimeout()))
+ .then(chain.filter(exchange));
}
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
index 53ea2623..b330aa50 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
@@ -23,7 +23,10 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import reactor.core.publisher.Mono;
+
import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
@@ -31,13 +34,10 @@ import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.server.ServerWebExchange;
-import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.WebHandler;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
-import reactor.core.publisher.Mono;
-
/**
* WebHandler that delegates to a chain of {@link GlobalFilter} instances and
* {@link GatewayFilterFactory} instances then to the target {@link WebHandler}.
@@ -82,7 +82,9 @@ public class FilteringWebHandler implements WebHandler {
//TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
- logger.debug("Sorted gatewayFilterFactories: "+ combined);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Sorted gatewayFilterFactories: "+ combined);
+ }
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
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 bcbe5d29..2b1d9ac0 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
@@ -61,7 +61,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
if (managmentPort != null && exchange.getRequest().getURI().getPort() == managmentPort.intValue()) {
return Mono.empty();
}
- exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName());
+ exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
@@ -146,4 +146,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
protected void validateRoute(Route route, ServerWebExchange exchange) {
}
+ protected String getSimpleName() {
+ return "RoutePredicateHandlerMapping";
+ }
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
index 507ec2d9..5003ed37 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.handler.predicate;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
@@ -40,6 +41,7 @@ import static org.springframework.http.server.PathContainer.parsePath;
*/
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory {
private static final Log log = LogFactory.getLog(RoutePredicateFactory.class);
+ private static final String MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY = "matchOptionalTrailingSeparator";
private PathPatternParser pathPatternParser = new PathPatternParser();
@@ -53,12 +55,13 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory shortcutFieldOrder() {
- return Collections.singletonList(PATTERN_KEY);
+ return Arrays.asList(PATTERN_KEY, MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY);
}
@Override
public Predicate apply(Config config) {
synchronized (this.pathPatternParser) {
+ pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchOptionalTrailingSeparator());
config.pathPattern = this.pathPatternParser.parse(config.pattern);
}
return exchange -> {
@@ -88,6 +91,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory c.setPattern(pattern)));
}
+ /**
+ * A predicate that checks if the path of the request matches the given pattern
+ * @param pattern the pattern to check the path against.
+ * The pattern is a {@link org.springframework.util.PathMatcher} pattern
+ * @param matchOptionalTrailingSeparator set to false if you do not want this path to match
+ * when there is a trailing /
+ * @return a {@link BooleanSpec} to be used to add logical operators
+ */
+ public BooleanSpec path(String pattern, boolean matchOptionalTrailingSeparator) {
+ return asyncPredicate(getBean(PathRoutePredicateFactory.class)
+ .applyAsync(c -> c.setPattern(pattern).setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator)));
+ }
+
/**
* This predicate is BETA and may be subject to change in a future release.
* A predicate that checks the contents of the request body
diff --git a/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000..c67b417b
--- /dev/null
+++ b/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,22 @@
+{
+ "properties": [
+ {
+ "name": "spring.cloud.gateway.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables gateway functionality.",
+ "defaultValue": "true"
+ },
+ {
+ "name": "spring.cloud.gateway.forwarded.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables the ForwardedHeadersFilter.",
+ "defaultValue": "true"
+ },
+ {
+ "name": "spring.cloud.gateway.metrics.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enables the collection of metrics data.",
+ "defaultValue": "false"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java
new file mode 100644
index 00000000..aefd24ae
--- /dev/null
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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 java.util.Map;
+
+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.cloud.gateway.test.BaseWebClientTests;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(properties = "spring.cloud.gateway.httpclient.response-timeout=3s", webEnvironment = RANDOM_PORT)
+@DirtiesContext
+@SuppressWarnings("unchecked")
+public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests {
+
+ @Test
+ public void responseTimeoutWorks() {
+ testClient.get()
+ .uri("/delay/5")
+ .exchange()
+ .expectStatus().is5xxServerError()
+ .expectBody(Map.class)
+ .consumeWith(result -> {
+ Map body = result.getResponseBody();
+ assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S");
+ });
+ }
+
+ @EnableAutoConfiguration
+ @SpringBootConfiguration
+ @Import(DefaultTestConfig.class)
+ public static class TestConfig {
+ }
+
+}
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
index 5603c75c..70e50843 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
@@ -44,6 +44,12 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test");
+
+ //since the configuration does not allow the trailing / to match this should fail
+ testClient.get().uri("/abc/123/function/")
+ .header(HttpHeaders.HOST, "www.path.org")
+ .exchange()
+ .expectStatus().is4xxClientError();
}
@Test
diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml
index 6ae8ed00..41e8bb5d 100644
--- a/spring-cloud-gateway-core/src/test/resources/application.yml
+++ b/spring-cloud-gateway-core/src/test/resources/application.yml
@@ -128,7 +128,7 @@ spring:
- id: path_test
uri: ${test.uri}
predicates:
- - Path=/{org}/{scope}/function
+ - Path=/{org}/{scope}/function,false
- Host=**.path.org
filters:
- SetPath=/anything/{org}{scope}
diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml
index 807035c1..032b78d7 100644
--- a/spring-cloud-gateway-dependencies/pom.xml
+++ b/spring-cloud-gateway-dependencies/pom.xml
@@ -5,7 +5,7 @@
spring-cloud-dependencies-parent
org.springframework.cloud
- 2.0.3.RELEASE
+ 2.0.4.BUILD-SNAPSHOT