From a0508bbe1e25af7e81771fd27d1779ff7cc4bd9b Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Wed, 15 Aug 2018 20:04:36 -0400 Subject: [PATCH 01/18] ReadBodyPredicate should set body to body of the original request, not the parsed object. --- .../config/GatewayAutoConfiguration.java | 4 +- .../predicate/ReadBodyPredicateFactory.java | 96 +++++++++++++------ 2 files changed, 68 insertions(+), 32 deletions(-) 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 7367455e..0ee16bc5 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 @@ -435,8 +435,8 @@ public class GatewayAutoConfiguration { } @Bean - public ReadBodyPredicateFactory readBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) { - return new ReadBodyPredicateFactory(codecConfigurer); + public ReadBodyPredicateFactory readBodyPredicateFactory() { + return new ReadBodyPredicateFactory(); } @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index 32ea6f9d..f4d32198 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -17,23 +17,31 @@ package org.springframework.cloud.gateway.handler.predicate; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.gateway.support.BodyInserterContext; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.cloud.gateway.handler.AsyncPredicate; -import org.springframework.cloud.gateway.support.DefaultServerRequest; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter.CACHED_REQUEST_BODY_KEY; @@ -47,11 +55,10 @@ public class ReadBodyPredicateFactory private static final String TEST_ATTRIBUTE = "read_body_predicate_test_attribute"; private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject"; - private final ServerCodecConfigurer codecConfigurer; + private static final List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); - public ReadBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) { + public ReadBodyPredicateFactory() { super(Config.class); - this.codecConfigurer = codecConfigurer; } @Override @@ -70,39 +77,48 @@ public class ReadBodyPredicateFactory try { boolean test = config.predicate.test(cachedBody); exchange.getAttributes().put(TEST_ATTRIBUTE, test); + 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); - }); - } - 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, + Flux origBody = exchange.getRequest().getBody().flatMap(dataBuffer -> { + ResolvableType type =ResolvableType.forClass(inClass); + for(HttpMessageReader messageReader: messageReaders) { + if(messageReader.canRead(type, exchange.getRequest().getHeaders().getContentType())) { + ReactiveHttpInputMessage inputMessage = new ReadBodyReactiveHttpInputMessage( + Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())), + exchange.getRequest().getHeaders()); + Function mapper = (bodyObj) -> { + exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, bodyObj); + boolean test = config.predicate.test(bodyObj); + exchange.getAttributes().put(TEST_ATTRIBUTE, test); + return Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())); + }; + return messageReader.read(type, inputMessage, Collections.EMPTY_MAP).flatMap(mapper); + } + } + return Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())); + }); + BodyInserter bodyInserter = BodyInserters.fromPublisher(origBody, DataBuffer.class); + 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); - })); + return Mono.just(test); + })); + + } }; } @@ -113,6 +129,26 @@ public class ReadBodyPredicateFactory "ReadBodyPredicateFactory is only async."); } + static class ReadBodyReactiveHttpInputMessage implements ReactiveHttpInputMessage { + private Flux body; + private HttpHeaders httpHeaders; + + public ReadBodyReactiveHttpInputMessage(Flux body, HttpHeaders headers) { + this.body = body; + this.httpHeaders = headers; + } + + @Override + public Flux getBody() { + return body; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } + } + public static class Config { private Class inClass; private Predicate predicate; From 31ac60d19720308317b1492070b448831321a520 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Fri, 17 Aug 2018 20:14:19 -0400 Subject: [PATCH 02/18] Use a Collector to collect the request body into a map containing two copies. --- .../predicate/ReadBodyPredicateFactory.java | 124 +++++++++++++----- 1 file changed, 89 insertions(+), 35 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index f4d32198..6ead30a2 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -18,10 +18,17 @@ package org.springframework.cloud.gateway.handler.predicate; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -37,7 +44,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.codec.HttpMessageReader; -import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -45,6 +51,8 @@ import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter.CACHED_REQUEST_BODY_KEY; +import static org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory.DataBufferMapCollector.BODY_ONE; +import static org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory.DataBufferMapCollector.BODY_TWO; /** * This predicate is BETA and may be subject to change in a future release. @@ -73,55 +81,101 @@ 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); return Mono.just(test); - } catch(ClassCastException e) { - if(LOGGER.isDebugEnabled()) { + } catch (ClassCastException e) { + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object", e); } } return Mono.just(false); } else { - Flux origBody = exchange.getRequest().getBody().flatMap(dataBuffer -> { - ResolvableType type =ResolvableType.forClass(inClass); - for(HttpMessageReader messageReader: messageReaders) { - if(messageReader.canRead(type, exchange.getRequest().getHeaders().getContentType())) { - ReactiveHttpInputMessage inputMessage = new ReadBodyReactiveHttpInputMessage( - Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())), - exchange.getRequest().getHeaders()); - Function mapper = (bodyObj) -> { - exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, bodyObj); - boolean test = config.predicate.test(bodyObj); - exchange.getAttributes().put(TEST_ATTRIBUTE, test); - return Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())); - }; - return messageReader.read(type, inputMessage, Collections.EMPTY_MAP).flatMap(mapper); - } - } - return Flux.just(dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer())); - }); - BodyInserter bodyInserter = BodyInserters.fromPublisher(origBody, DataBuffer.class); - 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); - })); + return exchange.getRequest().getBody().collect(new DataBufferMapCollector()).flatMap(dataBufferMap -> { + BodyInserter bodyInserter = BodyInserters.fromPublisher(dataBufferMap.get(BODY_ONE), DataBuffer.class); + CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, + exchange.getRequest().getHeaders()); + return bodyInserter.insert(outputMessage, new BodyInserterContext()) + // .log("modify_request", Level.INFO) + .then(Mono.defer(() -> { + ResolvableType type = ResolvableType.forClass(inClass); + for (HttpMessageReader messageReader : messageReaders) { + if (messageReader.canRead(type, exchange.getRequest().getHeaders().getContentType())) { + ReactiveHttpInputMessage inputMessage = new ReadBodyReactiveHttpInputMessage(dataBufferMap.get(BODY_TWO), + exchange.getRequest().getHeaders()); + Function mapper = (bodyObj) -> { + exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, bodyObj); + exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, + outputMessage.getBody()); + boolean test = config.predicate.test(bodyObj); + return Mono.just(test); + }; + return messageReader.readMono(type, inputMessage, Collections.EMPTY_MAP).flatMap(mapper); + } + } + return Mono.just(false); + })); + }); } }; } + /** + * This {@link Collector} is meant to collect the {@code Flux} from the request body into a {@link Map} + * which contains two copy of the body, one under the key {@code orig} and the other under the {@key copy}. + */ + class DataBufferMapCollector implements Collector>, Map>> { + public static final String BODY_ONE = "bodyOne"; + public static final String BODY_TWO = "bodyTwo"; + + @Override + public Supplier>> supplier() { + return () -> new HashMap>(); + } + + @Override + public BiConsumer>, DataBuffer> accumulator() { + return (dataBufferMap, dataBuffer) -> { + accumulate(BODY_ONE, dataBufferMap, dataBuffer); + accumulate(BODY_TWO, dataBufferMap, dataBuffer); + }; + } + + private void accumulate(String key, Map> dataBufferMap, DataBuffer dataBuffer) { + if (dataBufferMap.get(key) == null) { + dataBufferMap.put(key, Flux.just(copy(dataBuffer))); + } else { + dataBufferMap.put(key, dataBufferMap.get(key).mergeWith(Flux.just(copy(dataBuffer)))); + } + } + + @Override + public BinaryOperator>> combiner() { + return (map1, map2) -> { + map2.forEach((k, v) -> map1.merge(k, v, (v1, v2) -> v1.mergeWith(v2))); + return map1; + }; + } + + @Override + public Function>, Map>> finisher() { + return Function.identity(); + } + + @Override + public Set characteristics() { + return new HashSet(); + } + + private DataBuffer copy(DataBuffer dataBuffer) { + return dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer()); + } + } + @Override @SuppressWarnings("unchecked") public Predicate apply(Config config) { From 23d075852ba5f3d41144ff6c790104ddd32a0660 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Fri, 17 Aug 2018 20:27:22 -0400 Subject: [PATCH 03/18] Adding Characteristics to the Collector --- .../gateway/handler/predicate/ReadBodyPredicateFactory.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index 6ead30a2..9218f1f0 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.handler.predicate; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -131,6 +132,8 @@ public class ReadBodyPredicateFactory class DataBufferMapCollector implements Collector>, Map>> { public static final String BODY_ONE = "bodyOne"; public static final String BODY_TWO = "bodyTwo"; + private final Set CHARACTERISTICS = new HashSet<>(Arrays.asList( + Characteristics.IDENTITY_FINISH)); @Override public Supplier>> supplier() { @@ -168,7 +171,7 @@ public class ReadBodyPredicateFactory @Override public Set characteristics() { - return new HashSet(); + return CHARACTERISTICS; } private DataBuffer copy(DataBuffer dataBuffer) { From 2444776250fb49d6ce009618dbb8d97302ad967c Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Sat, 29 Sep 2018 14:54:48 -0400 Subject: [PATCH 04/18] Add support for configuring TLS handshake timeouts --- .../main/asciidoc/spring-cloud-gateway.adoc | 16 +++++++ .../config/GatewayAutoConfiguration.java | 3 ++ .../gateway/config/HttpClientProperties.java | 40 +++++++++++++++- .../test/ssl/SSLHandshakeTimeoutTests.java | 48 +++++++++++++++++++ .../cloud/gateway/test/ssl/SSLTests.java | 15 ++---- 5 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 95a8fda0..dacfe0a7 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -950,6 +950,22 @@ spring: - cert2.pem ---- +=== 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/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 145970c7..fd3837cb 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 @@ -166,6 +166,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) { 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 ca9159ea..5ba01001 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 @@ -220,8 +220,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; @@ -266,11 +271,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/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..852efb5d --- /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 SSLTests { + + @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/SSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java index c936eb75..42589d49 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.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 SSLTests 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 From bb1ea6a1234f4cba1a4708fa9b5a3574edc99ddc Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 5 Oct 2018 18:34:25 -0400 Subject: [PATCH 05/18] Only use scheme://host:port from routeUri fixes gh-465 --- .../filter/RouteToRequestUrlFilter.java | 9 ++++++--- .../filter/RouteToRequestUrlFilterTests.java | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) 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/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(); From 6a2f9d14b53ff3d2dd525cc296ca1639cf0ceff4 Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Sat, 29 Sep 2018 14:54:48 -0400 Subject: [PATCH 06/18] Add support for configuring TLS handshake timeouts --- .../main/asciidoc/spring-cloud-gateway.adoc | 19 +++++++- .../config/GatewayAutoConfiguration.java | 3 ++ .../gateway/config/HttpClientProperties.java | 40 +++++++++++++++- .../test/ssl/SSLHandshakeTimeoutTests.java | 48 +++++++++++++++++++ .../gateway/test/ssl/SingleCertSSLTests.java | 15 ++---- 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 31c919d0..63229ef8 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -950,7 +950,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/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 145970c7..fd3837cb 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 @@ -166,6 +166,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) { 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/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 From 709ef436f7f50ff1c33fdb3a056af5c475640c3c Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Mon, 15 Oct 2018 13:52:12 -0400 Subject: [PATCH 07/18] Added documentation about how to use the retry filter with the forward protocol. Fixes #356 --- docs/src/main/asciidoc/spring-cloud-gateway.adoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 95a8fda0..052af641 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -773,7 +773,10 @@ 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 URI that uses the `forward` protocol, the endpoint you forward to should +not return a `ResponseEntity` or else the retry filter will not be able to retry the request do to the response +already being committed in the exchange. Instead the endpoint should throw an exception and the retry filter +should be configured to retry on that exception. == Global Filters From bdb0913dd82c9399ccc985b2413d754a57fb501a Mon Sep 17 00:00:00 2001 From: Guido Lena Cota Date: Mon, 15 Oct 2018 23:05:26 +0200 Subject: [PATCH 08/18] Replace lambda with method reference --- .../cloud/gateway/actuate/GatewayControllerEndpoint.java | 2 +- .../cloud/gateway/route/CachingRouteDefinitionLocator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); } From 317edea8c2fa6bce44e73e36043175a470da7ef9 Mon Sep 17 00:00:00 2001 From: Guido Lena Cota Date: Mon, 15 Oct 2018 23:06:48 +0200 Subject: [PATCH 09/18] Minor refactors to improve readability --- .../cloud/gateway/filter/headers/HttpHeadersFilter.java | 6 +----- .../support/ipresolver/XForwardedRemoteAddressResolver.java | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java index d2301572..0e5644a9 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java @@ -62,10 +62,6 @@ public interface HttpHeadersFilter { } default boolean supports(Type type) { - if (type.equals(Type.REQUEST)) { - return true; - } - - return false; + return type.equals(Type.REQUEST); } } 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) { From cdef68af31c97fa0180ce90a6552567938e17b4d Mon Sep 17 00:00:00 2001 From: Guido Lena Cota Date: Mon, 15 Oct 2018 23:09:35 +0200 Subject: [PATCH 10/18] Use indexOf a char --- .../springframework/cloud/gateway/filter/FilterDefinition.java | 2 +- .../cloud/gateway/handler/predicate/PredicateDefinition.java | 2 +- .../springframework/cloud/gateway/route/RouteDefinition.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/handler/predicate/PredicateDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java index f202b9d4..f0798034 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java @@ -42,7 +42,7 @@ public class PredicateDefinition { } public PredicateDefinition(String text) { - int eqIdx = text.indexOf("="); + int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException("Unable to parse PredicateDefinition text '" + text + "'" + ", must be of the form name=value"); 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"); From eb13e0d68381cecf67bf5b75ccda8c38bc3c3720 Mon Sep 17 00:00:00 2001 From: Guido Lena Cota Date: Mon, 15 Oct 2018 23:10:20 +0200 Subject: [PATCH 11/18] Remove unused import, private methods, and private fields --- .../gateway/handler/predicate/PathRoutePredicateFactory.java | 1 - .../cloud/gateway/route/builder/GatewayFilterSpec.java | 3 --- .../cloud/gateway/support/DefaultServerResponse.java | 5 ----- .../cloud/gateway/support/ShortcutConfigurable.java | 1 - 4 files changed, 10 deletions(-) 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 5003ed37..d8d64ed9 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 @@ -18,7 +18,6 @@ package org.springframework.cloud.gateway.handler.predicate; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.function.Predicate; 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 cfa4cc7e..71d469f8 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 @@ -556,7 +556,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; From cca0901a5ea481c9b037f2e6727063d8e9b2d5e3 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Wed, 17 Oct 2018 10:51:02 -0400 Subject: [PATCH 12/18] Updates to using retry with forward protocol --- docs/src/main/asciidoc/spring-cloud-gateway.adoc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index e94f6007..a4345888 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -773,10 +773,7 @@ spring: statuses: BAD_GATEWAY ---- -NOTE: When using the retry filter with a URI that uses the `forward` protocol, the endpoint you forward to should -not return a `ResponseEntity` or else the retry filter will not be able to retry the request do to the response -already being committed in the exchange. Instead the endpoint should throw an exception and the retry filter -should be configured to retry on that exception. +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 From bdd970d562aa8b177bfe9d5ab674dddaa5987007 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Wed, 17 Oct 2018 11:17:46 -0400 Subject: [PATCH 13/18] Read the body twice by retaining it and using slice to give each read its own read/write indexes --- .../filter/AdaptCachedBodyGlobalFilter.java | 1 + .../predicate/ReadBodyPredicateFactory.java | 148 ++++-------------- 2 files changed, 31 insertions(+), 118 deletions(-) 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/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index 9218f1f0..05133916 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -17,43 +17,28 @@ package org.springframework.cloud.gateway.handler.predicate; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BinaryOperator; -import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.cloud.gateway.support.BodyInserterContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.cloud.gateway.handler.AsyncPredicate; -import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.codec.HttpMessageReader; -import org.springframework.web.reactive.function.BodyInserter; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter.CACHED_REQUEST_BODY_KEY; -import static org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory.DataBufferMapCollector.BODY_ONE; -import static org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory.DataBufferMapCollector.BODY_TWO; /** * This predicate is BETA and may be subject to change in a future release. @@ -95,90 +80,37 @@ public class ReadBodyPredicateFactory } return Mono.just(false); } else { - return exchange.getRequest().getBody().collect(new DataBufferMapCollector()).flatMap(dataBufferMap -> { - BodyInserter bodyInserter = BodyInserters.fromPublisher(dataBufferMap.get(BODY_ONE), DataBuffer.class); - CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, - exchange.getRequest().getHeaders()); - return bodyInserter.insert(outputMessage, new BodyInserterContext()) - // .log("modify_request", Level.INFO) - .then(Mono.defer(() -> { - ResolvableType type = ResolvableType.forClass(inClass); - for (HttpMessageReader messageReader : messageReaders) { - if (messageReader.canRead(type, exchange.getRequest().getHeaders().getContentType())) { - ReactiveHttpInputMessage inputMessage = new ReadBodyReactiveHttpInputMessage(dataBufferMap.get(BODY_TWO), - exchange.getRequest().getHeaders()); - Function mapper = (bodyObj) -> { - exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, bodyObj); - exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, - outputMessage.getBody()); - boolean test = config.predicate.test(bodyObj); - return Mono.just(test); - }; - return messageReader.readMono(type, inputMessage, Collections.EMPTY_MAP).flatMap(mapper); - } - } - return Mono.just(false); - })); + //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)); + }); - }); } }; } - /** - * This {@link Collector} is meant to collect the {@code Flux} from the request body into a {@link Map} - * which contains two copy of the body, one under the key {@code orig} and the other under the {@key copy}. - */ - class DataBufferMapCollector implements Collector>, Map>> { - public static final String BODY_ONE = "bodyOne"; - public static final String BODY_TWO = "bodyTwo"; - private final Set CHARACTERISTICS = new HashSet<>(Arrays.asList( - Characteristics.IDENTITY_FINISH)); - - @Override - public Supplier>> supplier() { - return () -> new HashMap>(); - } - - @Override - public BiConsumer>, DataBuffer> accumulator() { - return (dataBufferMap, dataBuffer) -> { - accumulate(BODY_ONE, dataBufferMap, dataBuffer); - accumulate(BODY_TWO, dataBufferMap, dataBuffer); - }; - } - - private void accumulate(String key, Map> dataBufferMap, DataBuffer dataBuffer) { - if (dataBufferMap.get(key) == null) { - dataBufferMap.put(key, Flux.just(copy(dataBuffer))); - } else { - dataBufferMap.put(key, dataBufferMap.get(key).mergeWith(Flux.just(copy(dataBuffer)))); - } - } - - @Override - public BinaryOperator>> combiner() { - return (map1, map2) -> { - map2.forEach((k, v) -> map1.merge(k, v, (v1, v2) -> v1.mergeWith(v2))); - return map1; - }; - } - - @Override - public Function>, Map>> finisher() { - return Function.identity(); - } - - @Override - public Set characteristics() { - return CHARACTERISTICS; - } - - private DataBuffer copy(DataBuffer dataBuffer) { - return dataBuffer.factory().allocateBuffer().write(dataBuffer.asByteBuffer()); - } - } - @Override @SuppressWarnings("unchecked") public Predicate apply(Config config) { @@ -186,26 +118,6 @@ public class ReadBodyPredicateFactory "ReadBodyPredicateFactory is only async."); } - static class ReadBodyReactiveHttpInputMessage implements ReactiveHttpInputMessage { - private Flux body; - private HttpHeaders httpHeaders; - - public ReadBodyReactiveHttpInputMessage(Flux body, HttpHeaders headers) { - this.body = body; - this.httpHeaders = headers; - } - - @Override - public Flux getBody() { - return body; - } - - @Override - public HttpHeaders getHeaders() { - return httpHeaders; - } - } - public static class Config { private Class inClass; private Predicate predicate; From b42c45fb8950d6be09ea22549a5b2ac72e0223c5 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 5 Oct 2018 14:20:59 -0400 Subject: [PATCH 14/18] Lazily inject DispatcherHandler. This prevents early initialization. --- .../config/GatewayAutoConfiguration.java | 4 +-- .../gateway/filter/ForwardRoutingFilter.java | 11 +++---- .../factory/HystrixGatewayFilterFactory.java | 29 ++++++++++--------- .../filter/ForwardRoutingFilterTests.java | 15 ++++++++-- .../HystrixGatewayFilterFactoryTests.java | 2 +- 5 files changed, 36 insertions(+), 25 deletions(-) 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 036613ca..3623a215 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 @@ -369,7 +369,7 @@ public class GatewayAutoConfiguration { @Bean @ConditionalOnBean(DispatcherHandler.class) - public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) { + public ForwardRoutingFilter forwardRoutingFilter(ObjectProvider dispatcherHandler) { return new ForwardRoutingFilter(dispatcherHandler); } @@ -496,7 +496,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/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/factory/HystrixGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java index 2b93514d..b0b7b71a 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,6 +23,17 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Function; +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 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.http.HttpStatus; @@ -34,21 +45,10 @@ 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; - /** * Depends on `spring-cloud-starter-netflix-hystrix`, {@see http://cloud.spring.io/spring-cloud-netflix/} * @author Spencer Gibb @@ -57,9 +57,9 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory dispatcherHandler; - public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) { + public HystrixGatewayFilterFactory(ObjectProvider dispatcherHandler) { super(Config.class); this.dispatcherHandler = dispatcherHandler; } @@ -149,7 +149,8 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory 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/factory/HystrixGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java index 7d18fc64..54773770 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 { From 027670277031cb8e446e7ab0aea1589ab8e6e428 Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Wed, 17 Oct 2018 23:09:33 +0200 Subject: [PATCH 15/18] Treat corner cases of Hystrix exception handling * Treat HttpClient timeouts the same as Hystrix timeouts * Propagate ResponseStatusException and throwables with @ResponseStatus when cause for COMMAND_EXECUTION failure Fix #554 --- .../factory/HystrixGatewayFilterFactory.java | 26 ++++++++++++++++--- .../gateway/support/TimeoutException.java | 5 ++++ .../NettyRoutingFilterIntegrationTests.java | 5 ++-- .../HystrixGatewayFilterFactoryTests.java | 23 +++++++++++++++- 4 files changed, 52 insertions(+), 7 deletions(-) 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 b0b7b71a..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 @@ -36,22 +36,24 @@ 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.http.HttpStatus; +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 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; /** * 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 { @@ -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); 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/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/factory/HystrixGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java index 54773770..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 @@ -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 From 6071a22ae081a18f0e987c65aca220a6ee2161af Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 23 Oct 2018 18:16:06 +0000 Subject: [PATCH 16/18] Update SNAPSHOT to 2.0.2.RELEASE --- docs/pom.xml | 2 +- pom.xml | 8 ++++---- spring-cloud-gateway-core/pom.xml | 2 +- spring-cloud-gateway-dependencies/pom.xml | 4 ++-- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 5a08ea0c..18ba2479 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.2.RELEASE spring-cloud-gateway-docs pom diff --git a/pom.xml b/pom.xml index 74e0848b..da05386c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-gateway - 2.0.2.BUILD-SNAPSHOT + 2.0.2.RELEASE pom Spring Cloud Gateway @@ -14,7 +14,7 @@ org.springframework.cloud spring-cloud-build - 2.0.4.BUILD-SNAPSHOT + 2.0.4.RELEASE @@ -48,8 +48,8 @@ UTF-8 UTF-8 1.8 - 2.0.1.BUILD-SNAPSHOT - 2.0.1.BUILD-SNAPSHOT + 2.0.2.RELEASE + 2.0.2.RELEASE diff --git a/spring-cloud-gateway-core/pom.xml b/spring-cloud-gateway-core/pom.xml index 9ee9fa14..acae9788 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.2.RELEASE .. spring-cloud-gateway-core diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 032b78d7..45142721 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.2.RELEASE pom spring-cloud-gateway-dependencies diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index 60d2e3f7..2178e9bf 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.2.RELEASE .. diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 3f77ca96..78c979ca 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.2.RELEASE .. diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index db50e26e..316528a4 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.2.RELEASE diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index 5d7388e7..f518c93b 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.2.RELEASE .. spring-cloud-starter-gateway From f49655946d31c9b0bdbfb07782689b0c2e8f2d1d Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 23 Oct 2018 18:21:14 +0000 Subject: [PATCH 17/18] Going back to snapshots --- docs/pom.xml | 2 +- pom.xml | 8 ++++---- spring-cloud-gateway-core/pom.xml | 2 +- spring-cloud-gateway-dependencies/pom.xml | 4 ++-- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 18ba2479..5a08ea0c 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-gateway - 2.0.2.RELEASE + 2.0.2.BUILD-SNAPSHOT spring-cloud-gateway-docs pom diff --git a/pom.xml b/pom.xml index da05386c..74e0848b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-gateway - 2.0.2.RELEASE + 2.0.2.BUILD-SNAPSHOT pom Spring Cloud Gateway @@ -14,7 +14,7 @@ org.springframework.cloud spring-cloud-build - 2.0.4.RELEASE + 2.0.4.BUILD-SNAPSHOT @@ -48,8 +48,8 @@ UTF-8 UTF-8 1.8 - 2.0.2.RELEASE - 2.0.2.RELEASE + 2.0.1.BUILD-SNAPSHOT + 2.0.1.BUILD-SNAPSHOT diff --git a/spring-cloud-gateway-core/pom.xml b/spring-cloud-gateway-core/pom.xml index acae9788..9ee9fa14 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.RELEASE + 2.0.2.BUILD-SNAPSHOT .. spring-cloud-gateway-core diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 45142721..032b78d7 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.RELEASE + 2.0.4.BUILD-SNAPSHOT spring-cloud-gateway-dependencies - 2.0.2.RELEASE + 2.0.2.BUILD-SNAPSHOT pom spring-cloud-gateway-dependencies diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index 2178e9bf..60d2e3f7 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.RELEASE + 2.0.2.BUILD-SNAPSHOT .. diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 78c979ca..3f77ca96 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.RELEASE + 2.0.2.BUILD-SNAPSHOT .. diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index 316528a4..db50e26e 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.RELEASE + 2.0.2.BUILD-SNAPSHOT diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index f518c93b..5d7388e7 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.RELEASE + 2.0.2.BUILD-SNAPSHOT .. spring-cloud-starter-gateway From d442440e7bad0178e96b90928461081b4b74ae7c Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 23 Oct 2018 18:21:14 +0000 Subject: [PATCH 18/18] Bumping versions to 2.0.3.BUILD-SNAPSHOT after release --- docs/pom.xml | 2 +- pom.xml | 4 ++-- spring-cloud-gateway-core/pom.xml | 2 +- spring-cloud-gateway-dependencies/pom.xml | 4 ++-- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) 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/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-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