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