Merge remote-tracking branch 'upstream/2.0.x' into rewrite_response_header_filter
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-gateway-docs</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Cloud Gateway</name>
|
||||
@@ -14,7 +14,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>2.0.4.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.4.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<scm>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-gateway-core</artifactId>
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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> 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> dispatcherHandler) {
|
||||
return new HystrixGatewayFilterFactory(dispatcherHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> trustedX509Certificates = new ArrayList<>();
|
||||
|
||||
// use netty default SSL timeouts
|
||||
private long handshakeTimeoutMillis = 10000L;
|
||||
private long closeNotifyFlushTimeoutMillis = 3000L;
|
||||
private long closeNotifyReadTimeoutMillis = 0L;
|
||||
|
||||
public List<String> 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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> dispatcherHandler;
|
||||
|
||||
public ForwardRoutingFilter(DispatcherHandler dispatcherHandler) {
|
||||
public ForwardRoutingFilter(ObjectProvider<DispatcherHandler> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HystrixGatewayFilterFactory.Config> {
|
||||
|
||||
public static final String FALLBACK_URI = "fallbackUri";
|
||||
|
||||
private final DispatcherHandler dispatcherHandler;
|
||||
private final ObjectProvider<DispatcherHandler> dispatcherHandler;
|
||||
|
||||
public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
|
||||
public HystrixGatewayFilterFactory(ObjectProvider<DispatcherHandler> dispatcherHandler) {
|
||||
super(Config.class);
|
||||
this.dispatcherHandler = dispatcherHandler;
|
||||
}
|
||||
@@ -101,8 +103,24 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<Hy
|
||||
}).onErrorResume((Function<Throwable, Mono<Void>>) 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<Hy
|
||||
|
||||
ServerHttpRequest request = this.exchange.getRequest().mutate().uri(requestUrl).build();
|
||||
ServerWebExchange mutated = exchange.mutate().request(request).build();
|
||||
return RxReactiveStreams.toObservable(HystrixGatewayFilterFactory.this.dispatcherHandler.handle(mutated));
|
||||
DispatcherHandler dispatcherHandler = HystrixGatewayFilterFactory.this.dispatcherHandler.getIfAvailable();
|
||||
return RxReactiveStreams.toObservable(dispatcherHandler.handle(mutated));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -17,22 +17,24 @@
|
||||
|
||||
package org.springframework.cloud.gateway.handler.predicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
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;
|
||||
|
||||
@@ -47,11 +49,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<HttpMessageReader<?>> 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<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
|
||||
|
||||
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||
@Override
|
||||
public Flux<DataBuffer> 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);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -569,7 +569,4 @@ public class GatewayFilterSpec extends UriSpec {
|
||||
}));
|
||||
}
|
||||
|
||||
private String routeId() {
|
||||
return routeBuilder.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> implements ServerResponse {
|
||||
|
||||
|
||||
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final BodyInserter<T, ? super ServerHttpResponse> inserter;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -91,7 +91,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver {
|
||||
public InetSocketAddress resolve(ServerWebExchange exchange) {
|
||||
List<String> 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<String> extractXForwardedValues(ServerWebExchange exchange) {
|
||||
List<String> 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) {
|
||||
|
||||
@@ -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<DispatcherHandler> 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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<parent>
|
||||
<artifactId>spring-cloud-dependencies-parent</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>2.0.4.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.4.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-cloud-gateway-dependencies</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>spring-cloud-gateway-dependencies</name>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-gateway</artifactId>
|
||||
<version>2.0.2.BUILD-SNAPSHOT</version>
|
||||
<version>2.0.3.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user