diff --git a/docs/pom.xml b/docs/pom.xml
index 5a08ea0c..05d56b55 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -5,7 +5,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
spring-cloud-gateway-docs
pom
diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
index d1bac5dc..f19defc2 100644
--- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
@@ -791,7 +791,7 @@ spring:
statuses: BAD_GATEWAY
----
-NOTE: At this time a URI using the `forward` protocol does not support using the retry filter.
+NOTE: When using the retry filter with a `forward:` prefixed URL, the target endpoint should be written carefully so that in case of an error it does not do anything that could result in a response being sent to the client and committed. For example, if the target endpoint is an annotated controller, the target controller method should not return `ResponseEntity` with an error status code. Instead it should throw an `Exception`, or signal an error, e.g. via a `Mono.error(ex)` return value, which the retry filter can be configured to handle by retrying.
== Global Filters
@@ -968,7 +968,24 @@ spring:
- cert2.pem
----
-If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore.
+If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore).
+
+=== TLS Handshake
+
+The Gateway maintains a client pool that it uses to route to backends. When communicating over https the client initiates a TLS handshake. A number of timeouts are assoicated with this handshake. These timeouts can be configured (defaults shown):
+
+.application.yml
+[source,yaml]
+----
+spring:
+ cloud:
+ gateway:
+ httpclient:
+ ssl:
+ handshake-timeout-millis: 10000
+ close-notify-flush-timeout-millis: 3000
+ close-notify-read-timeout-millis: 0
+----
== Configuration
diff --git a/pom.xml b/pom.xml
index 74e0848b..17f1ca8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
pom
Spring Cloud Gateway
@@ -14,7 +14,7 @@
org.springframework.cloud
spring-cloud-build
- 2.0.4.BUILD-SNAPSHOT
+ 2.0.4.RELEASE
diff --git a/spring-cloud-gateway-core/pom.xml b/spring-cloud-gateway-core/pom.xml
index 9ee9fa14..32def61b 100644
--- a/spring-cloud-gateway-core/pom.xml
+++ b/spring-cloud-gateway-core/pom.xml
@@ -6,7 +6,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
..
spring-cloud-gateway-core
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
index 52421b1f..b27cfd63 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java
@@ -182,7 +182,7 @@ http POST :8080/admin/gateway/routes/apiaddreqhead uri=http://httpbin.org:80 pre
return this.routeDefinitionLocator.getRouteDefinitions()
.filter(route -> route.getId().equals(id))
.singleOrEmpty()
- .map(route -> ResponseEntity.ok(route))
+ .map(ResponseEntity::ok)
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
}
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 e13b297f..d376e73d 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
@@ -167,6 +167,9 @@ public class GatewayAutoConfiguration {
// configure ssl
HttpClientProperties.Ssl ssl = properties.getSsl();
+ opts.sslHandshakeTimeoutMillis(ssl.getHandshakeTimeoutMillis());
+ opts.sslCloseNotifyFlushTimeoutMillis(ssl.getCloseNotifyFlushTimeoutMillis());
+ opts.sslCloseNotifyReadTimeoutMillis(ssl.getCloseNotifyReadTimeoutMillis());
X509Certificate[] trustedX509Certificates = ssl
.getTrustedX509CertificatesForTrustManager();
if (trustedX509Certificates.length > 0) {
@@ -367,7 +370,7 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnBean(DispatcherHandler.class)
- public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) {
+ public ForwardRoutingFilter forwardRoutingFilter(ObjectProvider dispatcherHandler) {
return new ForwardRoutingFilter(dispatcherHandler);
}
@@ -453,8 +456,8 @@ public class GatewayAutoConfiguration {
}
@Bean
- public ReadBodyPredicateFactory readBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) {
- return new ReadBodyPredicateFactory(codecConfigurer);
+ public ReadBodyPredicateFactory readBodyPredicateFactory() {
+ return new ReadBodyPredicateFactory();
}
@Bean
@@ -494,7 +497,7 @@ public class GatewayAutoConfiguration {
@ConditionalOnClass({HystrixObservableCommand.class, RxReactiveStreams.class})
protected static class HystrixConfiguration {
@Bean
- public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
+ public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(ObjectProvider dispatcherHandler) {
return new HystrixGatewayFilterFactory(dispatcherHandler);
}
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java
index 62ecd2e3..d479b3c1 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java
@@ -222,8 +222,13 @@ public class HttpClientProperties {
public class Ssl {
/** Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production. */
private boolean useInsecureTrustManager = false;
-
+
private List trustedX509Certificates = new ArrayList<>();
+
+ // use netty default SSL timeouts
+ private long handshakeTimeoutMillis = 10000L;
+ private long closeNotifyFlushTimeoutMillis = 3000L;
+ private long closeNotifyReadTimeoutMillis = 0L;
public List getTrustedX509Certificates() {
return trustedX509Certificates;
@@ -268,11 +273,42 @@ public class HttpClientProperties {
this.useInsecureTrustManager = useInsecureTrustManager;
}
+ public long getHandshakeTimeoutMillis() {
+ return handshakeTimeoutMillis;
+ }
+
+ public void setHandshakeTimeoutMillis(long handshakeTimeoutMillis) {
+ this.handshakeTimeoutMillis = handshakeTimeoutMillis;
+ }
+
+ public long getCloseNotifyFlushTimeoutMillis() {
+ return closeNotifyFlushTimeoutMillis;
+ }
+
+ public void setCloseNotifyFlushTimeoutMillis(long closeNotifyFlushTimeoutMillis) {
+ this.closeNotifyFlushTimeoutMillis = closeNotifyFlushTimeoutMillis;
+ }
+
+ public long getCloseNotifyReadTimeoutMillis() {
+ return closeNotifyReadTimeoutMillis;
+ }
+
+ public void setCloseNotifyReadTimeoutMillis(long closeNotifyReadTimeoutMillis) {
+ this.closeNotifyReadTimeoutMillis = closeNotifyReadTimeoutMillis;
+ }
+
@Override
public String toString() {
return "Ssl {useInsecureTrustManager=" + useInsecureTrustManager
- + ", trustedX509Certificates=" + trustedX509Certificates + "}";
+ + ", trustedX509Certificates=" + trustedX509Certificates
+ + ", handshakeTimeoutMillis=" + handshakeTimeoutMillis
+ + ", closeNotifyFlushTimeoutMillis="
+ + closeNotifyFlushTimeoutMillis
+ + ", closeNotifyReadTimeoutMillis="
+ + closeNotifyReadTimeoutMillis + "}";
}
+
+
}
@Override
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java
index 16760f23..a7f758b2 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java
@@ -39,6 +39,7 @@ public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered {
return body;
}
};
+ exchange.getAttributes().remove(CACHED_REQUEST_BODY_KEY);
return chain.filter(exchange.mutate().request(decorator).build());
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java
index 5923e871..e3545ddf 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java
@@ -41,7 +41,7 @@ public class FilterDefinition {
}
public FilterDefinition(String text) {
- int eqIdx = text.indexOf("=");
+ int eqIdx = text.indexOf('=');
if (eqIdx <= 0) {
setName(text);
return;
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java
index 50e81c8b..156225cc 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java
@@ -4,6 +4,9 @@ import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.Ordered;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
@@ -12,15 +15,13 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted;
-import reactor.core.publisher.Mono;
-
public class ForwardRoutingFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class);
- private final DispatcherHandler dispatcherHandler;
+ private final ObjectProvider dispatcherHandler;
- public ForwardRoutingFilter(DispatcherHandler dispatcherHandler) {
+ public ForwardRoutingFilter(ObjectProvider dispatcherHandler) {
this.dispatcherHandler = dispatcherHandler;
}
@@ -45,6 +46,6 @@ public class ForwardRoutingFilter implements GlobalFilter, Ordered {
log.trace("Forwarding to URI: "+requestUrl);
}
- return this.dispatcherHandler.handle(exchange);
+ return this.dispatcherHandler.getIfAvailable().handle(exchange);
}
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java
index fd879517..e76046e5 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java
@@ -68,11 +68,14 @@ public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}
- URI requestUrl = UriComponentsBuilder.fromUri(uri)
- .uri(routeUri)
+ URI mergedUrl = UriComponentsBuilder.fromUri(uri)
+ // .uri(routeUri)
+ .scheme(routeUri.getScheme())
+ .host(routeUri.getHost())
+ .port(routeUri.getPort())
.build(encoded)
.toUri();
- exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
+ exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
return chain.filter(exchange);
}
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 2b93514d..90d22906 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
@@ -23,43 +23,45 @@ import java.util.List;
import java.util.function.Consumer;
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.util.Assert;
-import org.springframework.util.StringUtils;
-import org.springframework.web.reactive.DispatcherHandler;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.server.ServerWebExchange;
-import org.springframework.web.util.UriComponentsBuilder;
-
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixObservableCommand;
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.containsEncodedParts;
-
import reactor.core.publisher.Mono;
import rx.Observable;
import rx.RxReactiveStreams;
import rx.Subscription;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.support.TimeoutException;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.reactive.DispatcherHandler;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_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
*/
public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory {
public static final String FALLBACK_URI = "fallbackUri";
- private final DispatcherHandler dispatcherHandler;
+ private final ObjectProvider dispatcherHandler;
- public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
+ public HystrixGatewayFilterFactory(ObjectProvider dispatcherHandler) {
super(Config.class);
this.dispatcherHandler = dispatcherHandler;
}
@@ -101,8 +103,24 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory>) throwable -> {
if (throwable instanceof HystrixRuntimeException) {
HystrixRuntimeException e = (HystrixRuntimeException) throwable;
- if (e.getFailureType() == TIMEOUT) {
- return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT));
+ HystrixRuntimeException.FailureType failureType = e.getFailureType();
+
+ switch (failureType) {
+ case TIMEOUT:
+ return Mono.error(new TimeoutException());
+ case COMMAND_EXCEPTION: {
+ Throwable cause = e.getCause();
+
+ /*
+ * We forsake here the null check for cause as HystrixRuntimeException will
+ * always have a cause if the failure type is COMMAND_EXCEPTION.
+ */
+ if (cause instanceof ResponseStatusException || AnnotatedElementUtils
+ .findMergedAnnotation(cause.getClass(), ResponseStatus.class) != null) {
+ return Mono.error(cause);
+ }
+ }
+ default: break;
}
}
return Mono.error(throwable);
@@ -149,7 +167,8 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory> messageReaders = HandlerStrategies.withDefaults().messageReaders();
- public ReadBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) {
+ public ReadBodyPredicateFactory() {
super(Config.class);
- this.codecConfigurer = codecConfigurer;
}
@Override
@@ -66,43 +67,47 @@ public class ReadBodyPredicateFactory
// exception will be thrown. The below if/else caches the body object as a request attribute in the ServerWebExchange
// so if this filter is run more than once (due to more than one route using it) we do not try to read the
// request body multiple times
- if(cachedBody != null) {
+ if (cachedBody != null) {
try {
boolean test = config.predicate.test(cachedBody);
exchange.getAttributes().put(TEST_ATTRIBUTE, test);
- } catch(ClassCastException e) {
- if(LOGGER.isDebugEnabled()) {
+ return Mono.just(test);
+ } catch (ClassCastException e) {
+ if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object",
e);
}
}
- modifiedBody = Mono.just(cachedBody);
+ return Mono.just(false);
} else {
- ServerRequest serverRequest = new DefaultServerRequest(exchange);
- // TODO: flux or mono
- modifiedBody = serverRequest.bodyToMono(inClass)
- // .log("modify_request_mono", Level.INFO)
- .flatMap(body -> {
- // TODO: migrate to async
- exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, body);
- boolean test = config.predicate.test(body);
- exchange.getAttributes().put(TEST_ATTRIBUTE, test);
- return Mono.just(body);
+ //Join all the DataBuffers so we have a single DataBuffer for the body
+ return DataBufferUtils.join(exchange.getRequest().getBody())
+ .flatMap(dataBuffer -> {
+ //Update the retain counts so we can read the body twice, once to parse into an object
+ //that we can test the predicate against and a second time when the HTTP client sends
+ //the request downstream
+ //Note: if we end up reading the body twice we will run into a problem, but as of right
+ //now there is no good use case for doing this
+ DataBufferUtils.retain(dataBuffer);
+ //Make a slice for each read so each read has its own read/write indexes
+ Flux cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
+
+ ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
+ @Override
+ public Flux getBody() {
+ return cachedFlux;
+ }
+ };
+ return ServerRequest.create(exchange.mutate().request(mutatedRequest).build(), messageReaders)
+ .bodyToMono(inClass)
+ .doOnNext(objectValue -> {
+ exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue);
+ exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, cachedFlux);
+ })
+ .map(objectValue -> config.predicate.test(objectValue));
});
+
}
- BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, inClass);
- CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
- exchange.getRequest().getHeaders());
- return bodyInserter.insert(outputMessage, new BodyInserterContext())
- // .log("modify_request", Level.INFO)
- .then(Mono.defer(() -> {
- boolean test = (Boolean) exchange.getAttributes()
- .getOrDefault(TEST_ATTRIBUTE, Boolean.FALSE);
- exchange.getAttributes().remove(TEST_ATTRIBUTE);
- exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY,
- outputMessage.getBody());
- return Mono.just(test);
- }));
};
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java
index 62091d2a..71989463 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java
@@ -39,7 +39,7 @@ public class CachingRouteDefinitionLocator implements RouteDefinitionLocator {
public CachingRouteDefinitionLocator(RouteDefinitionLocator delegate) {
this.delegate = delegate;
routeDefinitions = CacheFlux.lookup(cache, "routeDefs", RouteDefinition.class)
- .onCacheMissResume(() -> this.delegate.getRouteDefinitions());
+ .onCacheMissResume(this.delegate::getRouteDefinitions);
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java
index c5ef5f47..f34c6b1a 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java
@@ -56,7 +56,7 @@ public class RouteDefinition {
public RouteDefinition() {}
public RouteDefinition(String text) {
- int eqIdx = text.indexOf("=");
+ int eqIdx = text.indexOf('=');
if (eqIdx <= 0) {
throw new ValidationException("Unable to parse RouteDefinition text '" + text + "'" +
", must be of the form name=value");
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 4d38cd64..74d207e3 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
@@ -569,7 +569,4 @@ public class GatewayFilterSpec extends UriSpec {
}));
}
- private String routeId() {
- return routeBuilder.getId();
- }
}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java
index dbc03da1..13d390c2 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java
@@ -17,18 +17,15 @@
package org.springframework.cloud.gateway.support;
-import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.codec.HttpMessageWriter;
@@ -43,8 +40,6 @@ import org.springframework.web.server.ServerWebExchange;
public class DefaultServerResponse implements ServerResponse {
- private static final Set SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
-
private final ServerWebExchange exchange;
private final BodyInserter inserter;
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
index c2f0679a..13b3ead8 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
@@ -25,7 +25,6 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.Assert;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java
index 8d429bc2..2a8e518c 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java
@@ -17,6 +17,11 @@
package org.springframework.cloud.gateway.support;
+import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT;
+
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = GATEWAY_TIMEOUT, reason = "Response took longer than configured timeout")
public class TimeoutException extends Exception {
public TimeoutException() {
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java
index 6589d903..83472604 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java
@@ -91,7 +91,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver {
public InetSocketAddress resolve(ServerWebExchange exchange) {
List xForwardedValues = extractXForwardedValues(exchange);
Collections.reverse(xForwardedValues);
- if (xForwardedValues.size() != 0) {
+ if (!xForwardedValues.isEmpty()) {
int index = Math.min(xForwardedValues.size(), maxTrustedIndex) - 1;
return InetSocketAddress.createUnresolved(xForwardedValues.get(index), 0);
}
@@ -101,7 +101,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver {
private List extractXForwardedValues(ServerWebExchange exchange) {
List xForwardedValues = exchange.getRequest().getHeaders()
.get(X_FORWARDED_FOR);
- if (xForwardedValues == null || xForwardedValues.size() == 0) {
+ if (xForwardedValues == null || xForwardedValues.isEmpty()) {
return Collections.emptyList();
}
if (xForwardedValues.size() > 1) {
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java
index 59f8643f..e2eac286 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java
@@ -1,5 +1,7 @@
package org.springframework.cloud.gateway.filter;
+import java.net.URI;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -7,6 +9,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.Ordered;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
@@ -14,10 +18,11 @@ import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
-import java.net.URI;
-
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
@@ -33,6 +38,9 @@ public class ForwardRoutingFilterTests {
@Mock
private GatewayFilterChain chain;
+ @Mock
+ private ObjectProvider objectProvider;
+
@Mock
private DispatcherHandler dispatcherHandler;
@@ -42,6 +50,7 @@ public class ForwardRoutingFilterTests {
@Before
public void setup() {
exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localendpoint").build());
+ when(objectProvider.getIfAvailable()).thenReturn(this.dispatcherHandler);
}
@Test
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
index aefd24ae..4148a7ba 100644
--- 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
@@ -27,6 +27,7 @@ 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.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@@ -44,11 +45,11 @@ public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests {
testClient.get()
.uri("/delay/5")
.exchange()
- .expectStatus().is5xxServerError()
+ .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT)
.expectBody(Map.class)
.consumeWith(result -> {
Map body = result.getResponseBody();
- assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S");
+ assertThat(body).containsEntry("message", "Response took longer than configured timeout");
});
}
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java
index d857f6ae..97ec6c5a 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java
@@ -48,9 +48,10 @@ public class RouteToRequestUrlFilterTests {
.get("http://localhost/get?a=b")
.build();
- ServerWebExchange webExchange = testFilter(request, "http://myhost");
+ ServerWebExchange webExchange = testFilter(request, "http://myhost/mypath");
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(uri).hasScheme("http").hasHost("myhost")
+ .hasPath("/get")
.hasParameter("a", "b");
}
@@ -60,6 +61,17 @@ public class RouteToRequestUrlFilterTests {
.get("http://localhost/getb")
.build();
+ ServerWebExchange webExchange = testFilter(request, "lb://myhost");
+ URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
+ assertThat(uri).hasScheme("lb").hasHost("myhost");
+ }
+
+ @Test
+ public void happyPathLbPlusScheme() {
+ MockServerHttpRequest request = MockServerHttpRequest
+ .get("http://localhost/getb")
+ .build();
+
ServerWebExchange webExchange = testFilter(request, "lb:http://myhost");
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(uri).hasScheme("http").hasHost("myhost");
@@ -114,7 +126,7 @@ public class RouteToRequestUrlFilterTests {
.method(HttpMethod.GET, url)
.build();
- ServerWebExchange webExchange = testFilter(request, "http://myhost");
+ ServerWebExchange webExchange = testFilter(request, "http://myhost/abc%20def/get");
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(uri).hasScheme("http").hasHost("myhost")
.hasPath("/abc def/get");
@@ -170,9 +182,9 @@ public class RouteToRequestUrlFilterTests {
}
}
- private ServerWebExchange testFilter(MockServerHttpRequest request, String url) {
+ private ServerWebExchange testFilter(MockServerHttpRequest request, String routeUri) {
Route value = Route.async().id("1")
- .uri(URI.create(url))
+ .uri(URI.create(routeUri))
.order(0)
.predicate(swe -> true)
.build();
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 7d18fc64..a47faecf 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
@@ -50,7 +50,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
import static org.springframework.http.MediaType.TEXT_HTML;
@RunWith(SpringRunner.class)
-@SpringBootTest(webEnvironment = RANDOM_PORT)
+@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "debug=true")
@DirtiesContext
public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
@@ -74,6 +74,20 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
.isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value()));
}
+ /*
+ * Tests that timeouts bubbling from the underpinning WebClient are treated the same as
+ * Hystrix timeouts in terms of outside response. (Internally, timeouts from the WebClient
+ * are seen as command failures and trigger the opening of circuit breakers the same way
+ * timeouts do; it may be confusing in terms of the Hystrix metrics though)
+ */
+ @Test
+ public void hystrixTimeoutFromWebClient() {
+ testClient.get().uri("/delay/10")
+ .header("Host", "www.hystrixresponsestall.org")
+ .exchange()
+ .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT);
+ }
+
@Test
public void hystrixFilterFallback() {
testClient.get().uri("/delay/3?a=b")
@@ -157,11 +171,18 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
.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
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java
new file mode 100644
index 00000000..b6c6ebef
--- /dev/null
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013-2017 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.test.ssl;
+
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.JsonPathAssertions;
+import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
+
+@RunWith(SpringRunner.class)
+// this test works because it assumes TLS hand shake cannot be done in 1ms. It takes closer to 80ms
+@SpringBootTest(webEnvironment = RANDOM_PORT, properties = {"spring.cloud.gateway.httpclient.ssl.handshake-timeout-millis=1"})
+@DirtiesContext
+@ActiveProfiles("ssl")
+public class SSLHandshakeTimeoutTests extends SingleCertSSLTests {
+
+ @Test
+ @Override // here we validate that it the handshake times out
+ public void testSslTrust() {
+ ResponseSpec responseSpec = testClient.get().uri("/ssltrust").exchange();
+ responseSpec.expectStatus().is5xxServerError();
+ JsonPathAssertions jsonPath = responseSpec.expectBody().jsonPath("message");
+ jsonPath.isEqualTo("handshake timed out");
+ }
+
+
+}
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java
index c351a0b5..824b97c3 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java
@@ -17,7 +17,6 @@
package org.springframework.cloud.gateway.test.ssl;
-import static org.junit.Assert.assertTrue;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import javax.net.ssl.SSLException;
@@ -30,16 +29,15 @@ 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.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import io.netty.handler.ssl.SslContext;
@@ -61,20 +59,17 @@ public class SingleCertSSLTests extends BaseWebClientTests {
opt -> opt.sslContext(sslContext));
baseUri = "https://localhost:" + port;
this.webClient = WebClient.builder().clientConnector(httpConnector)
- .baseUrl(baseUri).build();
+ .baseUrl(baseUri).build();
+ this.testClient = WebTestClient.bindToServer(httpConnector).baseUrl(baseUri).build();
}
catch (SSLException e) {
throw new RuntimeException(e);
}
}
-
+
@Test
public void testSslTrust() {
- ClientResponse clientResponse = webClient.get().uri("/ssltrust")
- .exchange().block();
- HttpStatus statusCode = clientResponse.statusCode();
- assertTrue(statusCode.is2xxSuccessful());
-
+ testClient.get().uri("/ssltrust").exchange().expectStatus().is2xxSuccessful();
}
@EnableAutoConfiguration
diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml
index 032b78d7..70895793 100644
--- a/spring-cloud-gateway-dependencies/pom.xml
+++ b/spring-cloud-gateway-dependencies/pom.xml
@@ -5,12 +5,12 @@
spring-cloud-dependencies-parent
org.springframework.cloud
- 2.0.4.BUILD-SNAPSHOT
+ 2.0.4.RELEASE
spring-cloud-gateway-dependencies
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
pom
spring-cloud-gateway-dependencies
diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml
index 60d2e3f7..50edc79f 100644
--- a/spring-cloud-gateway-mvc/pom.xml
+++ b/spring-cloud-gateway-mvc/pom.xml
@@ -10,7 +10,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
..
diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml
index 3f77ca96..2a66cae2 100644
--- a/spring-cloud-gateway-sample/pom.xml
+++ b/spring-cloud-gateway-sample/pom.xml
@@ -16,7 +16,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
..
diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml
index db50e26e..19b63469 100644
--- a/spring-cloud-gateway-webflux/pom.xml
+++ b/spring-cloud-gateway-webflux/pom.xml
@@ -10,7 +10,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml
index 5d7388e7..ab501c12 100644
--- a/spring-cloud-starter-gateway/pom.xml
+++ b/spring-cloud-starter-gateway/pom.xml
@@ -5,7 +5,7 @@
org.springframework.cloud
spring-cloud-gateway
- 2.0.2.BUILD-SNAPSHOT
+ 2.0.3.BUILD-SNAPSHOT
..
spring-cloud-starter-gateway