From 76d138fb40877a6f1a986ee351015570dc184de2 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Thu, 13 Jun 2019 15:08:51 -0400 Subject: [PATCH] Adds ability to cache body without converting to bytes (#1095) Creates new Cache filter that is activated with retry filter is used. Other filters can opt into caching if needed. Uses netty factory and retainedSlice() for retry. Only cache if retry filter is enabled Fixes gh-982 Fixes gh-1064 --- pom.xml | 2 +- .../config/GatewayAutoConfiguration.java | 15 +++ .../gateway/event/EnableBodyCachingEvent.java | 34 ++++++ .../filter/AlwaysRetainBodyGlobalFilter.java | 101 ++++++++++++++++++ .../filter/RemoveCachedBodyFilter.java | 52 +++++++++ .../factory/AbstractGatewayFilterFactory.java | 15 ++- .../filter/factory/GatewayFilterFactory.java | 15 +++ .../factory/HystrixGatewayFilterFactory.java | 2 + .../RequestSizeGatewayFilterFactory.java | 8 +- .../factory/RetryGatewayFilterFactory.java | 30 +++++- .../route/RouteDefinitionRouteLocator.java | 4 +- .../route/builder/GatewayFilterSpec.java | 12 ++- .../cloud/gateway/support/HasRouteId.java | 25 +++++ .../config/GatewayAutoConfigurationTests.java | 3 +- .../RequestSizeGatewayFilterFactoryTest.java | 12 +-- ...yGatewayFilterFactoryIntegrationTests.java | 11 +- .../gateway/test/FormIntegrationTests.java | 25 +++-- spring-cloud-gateway-dependencies/pom.xml | 2 +- 18 files changed, 328 insertions(+), 40 deletions(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/EnableBodyCachingEvent.java create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AlwaysRetainBodyGlobalFilter.java create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RemoveCachedBodyFilter.java create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/HasRouteId.java diff --git a/pom.xml b/pom.xml index 72b8c51f..be6e4b30 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.cloud spring-cloud-build - 2.1.4.BUILD-SNAPSHOT + 2.1.6.BUILD-SNAPSHOT diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 856800e8..b1221eca 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 @@ -45,11 +45,13 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter; +import org.springframework.cloud.gateway.filter.AlwaysRetainBodyGlobalFilter; import org.springframework.cloud.gateway.filter.ForwardPathFilter; import org.springframework.cloud.gateway.filter.ForwardRoutingFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.NettyRoutingFilter; import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; +import org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.WebsocketRoutingFilter; import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter; @@ -261,6 +263,16 @@ public class GatewayAutoConfiguration { return new AdaptCachedBodyGlobalFilter(); } + @Bean + public AlwaysRetainBodyGlobalFilter alwaysRetainBodyGlobalFilter() { + return new AlwaysRetainBodyGlobalFilter(); + } + + @Bean + public RemoveCachedBodyFilter removeCachedBodyFilter() { + return new RemoveCachedBodyFilter(); + } + @Bean public RouteToRequestUrlFilter routeToRequestUrlFilter() { return new RouteToRequestUrlFilter(); @@ -588,6 +600,9 @@ public class GatewayAutoConfiguration { }); } + //TODO: add configuration to turn on wiretap + //httpClient = httpClient.wiretap(true); + return httpClient; } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/EnableBodyCachingEvent.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/EnableBodyCachingEvent.java new file mode 100644 index 00000000..c4c07563 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/EnableBodyCachingEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2019 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 + * + * https://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.event; + +import org.springframework.context.ApplicationEvent; + +public class EnableBodyCachingEvent extends ApplicationEvent { + + private final String routeId; + + public EnableBodyCachingEvent(Object source, String routeId) { + super(source); + this.routeId = routeId; + } + + public String getRouteId() { + return this.routeId; + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AlwaysRetainBodyGlobalFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AlwaysRetainBodyGlobalFilter.java new file mode 100644 index 00000000..d5d4f78c --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AlwaysRetainBodyGlobalFilter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2019 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.event.EnableBodyCachingEvent; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.NettyDataBuffer; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +public class AlwaysRetainBodyGlobalFilter + implements GlobalFilter, Ordered, ApplicationListener { + + private static final Log log = LogFactory.getLog(AlwaysRetainBodyGlobalFilter.class); + + private ConcurrentMap routesToCache = new ConcurrentHashMap<>(); + + /** + * Request body cache key. + */ + public static final String ALWAYS_CACHE_REQUEST_BODY_KEY = "alwaysCacheRequestBody"; + + @Override + public void onApplicationEvent(EnableBodyCachingEvent event) { + this.routesToCache.putIfAbsent(event.getRouteId(), true); + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + Object body = exchange.getAttributeOrDefault(ALWAYS_CACHE_REQUEST_BODY_KEY, null); + Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + + if (body != null || !this.routesToCache.containsKey(route.getId())) { + return chain.filter(exchange); + } + + return DataBufferUtils.join(exchange.getRequest().getBody()) + .flatMap(dataBuffer -> { + if (dataBuffer.readableByteCount() > 0) { + if (log.isTraceEnabled()) { + log.trace("retaining body in exchange attribute"); + } + exchange.getAttributes().put(ALWAYS_CACHE_REQUEST_BODY_KEY, + dataBuffer); + } + + ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator( + exchange.getRequest()) { + @Override + public Flux getBody() { + return Mono.fromSupplier(() -> { + if (exchange.getAttributeOrDefault( + ALWAYS_CACHE_REQUEST_BODY_KEY, null) == null) { + // probably == downstream closed + return null; + } + // TODO: deal with Netty + NettyDataBuffer pdb = (NettyDataBuffer) dataBuffer; + return pdb.factory() + .wrap(pdb.getNativeBuffer().retainedSlice()); + }).flux(); + } + }; + return chain.filter(exchange.mutate().request(decorator).build()); + }).switchIfEmpty(chain.filter(exchange)); + } + + @Override + public int getOrder() { + return -10; + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RemoveCachedBodyFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RemoveCachedBodyFilter.java new file mode 100644 index 00000000..9411cb3d --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RemoveCachedBodyFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2019 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.PooledDataBuffer; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.filter.AlwaysRetainBodyGlobalFilter.ALWAYS_CACHE_REQUEST_BODY_KEY; + +public class RemoveCachedBodyFilter implements GlobalFilter, Ordered { + + private static final Log log = LogFactory.getLog(RemoveCachedBodyFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return chain.filter(exchange).doFinally(s -> { + PooledDataBuffer b = (PooledDataBuffer) exchange.getAttributes() + .remove(ALWAYS_CACHE_REQUEST_BODY_KEY); + if (b != null && b.isAllocated()) { + if (log.isTraceEnabled()) { + log.trace("releasing cached body in exchange attribute"); + } + b.release(); + } + }); + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java index a4c63683..abeea4fa 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/AbstractGatewayFilterFactory.java @@ -17,6 +17,8 @@ package org.springframework.cloud.gateway.filter.factory; import org.springframework.cloud.gateway.support.AbstractConfigurable; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; /** * This class is BETA and may be subject to change in a future release. @@ -24,7 +26,9 @@ import org.springframework.cloud.gateway.support.AbstractConfigurable; * @param {@link AbstractConfigurable} subtype */ public abstract class AbstractGatewayFilterFactory extends AbstractConfigurable - implements GatewayFilterFactory { + implements GatewayFilterFactory, ApplicationEventPublisherAware { + + private ApplicationEventPublisher publisher; @SuppressWarnings("unchecked") public AbstractGatewayFilterFactory() { @@ -35,6 +39,15 @@ public abstract class AbstractGatewayFilterFactory extends AbstractConfigurab super(configClass); } + protected ApplicationEventPublisher getPublisher() { + return this.publisher; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + public static class NameConfig { private String name; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilterFactory.java index e8432dcb..2635f4e3 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilterFactory.java @@ -20,6 +20,7 @@ import java.util.function.Consumer; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.support.Configurable; +import org.springframework.cloud.gateway.support.HasRouteId; import org.springframework.cloud.gateway.support.NameUtils; import org.springframework.cloud.gateway.support.ShortcutConfigurable; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -41,6 +42,12 @@ public interface GatewayFilterFactory extends ShortcutConfigurable, Configura String VALUE_KEY = "value"; // useful for javadsl + default GatewayFilter apply(String routeId, Consumer consumer) { + C config = newConfig(); + consumer.accept(config); + return apply(routeId, config); + } + default GatewayFilter apply(Consumer consumer) { C config = newConfig(); consumer.accept(config); @@ -58,6 +65,14 @@ public interface GatewayFilterFactory extends ShortcutConfigurable, Configura GatewayFilter apply(C config); + default GatewayFilter apply(String routeId, C config) { + if (config instanceof HasRouteId) { + HasRouteId hasRouteId = (HasRouteId) config; + hasRouteId.setRouteId(routeId); + } + return apply(config); + } + default String name() { // TODO: deal with proxys return NameUtils.normalizeFilterFactoryName(getClass()); 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 c4688538..db20c193 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 @@ -86,6 +86,8 @@ public class HystrixGatewayFilterFactory return singletonList(NAME_KEY); } + @Override + // TODO: make Config implement HasRouteId and remove this method. public GatewayFilter apply(String routeId, Consumer consumer) { Config config = newConfig(); consumer.accept(config); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactory.java index 70292873..d3d5cb72 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactory.java @@ -66,9 +66,11 @@ public class RequestSizeGatewayFilterFactory extends Long currentRequestSize = Long.valueOf(contentLength); if (currentRequestSize > requestSizeConfig.getMaxSize()) { exchange.getResponse().setStatusCode(HttpStatus.PAYLOAD_TOO_LARGE); - exchange.getResponse().getHeaders().add("errorMessage", - getErrorMessage(currentRequestSize, - requestSizeConfig.getMaxSize())); + if (!exchange.getResponse().isCommitted()) { + exchange.getResponse().getHeaders().add("errorMessage", + getErrorMessage(currentRequestSize, + requestSizeConfig.getMaxSize())); + } return exchange.getResponse().setComplete(); } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 8115e41a..0c7096fe 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -33,7 +33,9 @@ import reactor.retry.RepeatContext; import reactor.retry.Retry; import reactor.retry.RetryContext; +import org.springframework.cloud.gateway.event.EnableBodyCachingEvent; import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.support.HasRouteId; import org.springframework.cloud.gateway.support.TimeoutException; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -130,7 +132,7 @@ public class RetryGatewayFilterFactory .retryMax(retryConfig.getRetries()); } - return apply(statusCodeRepeat, exceptionRetry); + return apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry); } public boolean exceedsMaxIterations(ServerWebExchange exchange, @@ -145,7 +147,7 @@ public class RetryGatewayFilterFactory } public void reset(ServerWebExchange exchange) { - // TODO: what else to do to reset SWE? + // TODO: what else to do to reset exchange? Set addedHeaders = exchange.getAttributeOrDefault( CLIENT_RESPONSE_HEADER_NAMES, Collections.emptySet()); addedHeaders @@ -153,8 +155,18 @@ public class RetryGatewayFilterFactory exchange.getAttributes().remove(GATEWAY_ALREADY_ROUTED_ATTR); } + @Deprecated public GatewayFilter apply(Repeat repeat, Retry retry) { + return apply(null, repeat, retry); + } + + public GatewayFilter apply(String routeId, Repeat repeat, + Retry retry) { + if (routeId != null && getPublisher() != null) { + // send an event to enable caching + getPublisher().publishEvent(new EnableBodyCachingEvent(this, routeId)); + } return (exchange, chain) -> { trace("Entering retry-filter"); @@ -193,7 +205,9 @@ public class RetryGatewayFilterFactory } @SuppressWarnings("unchecked") - public static class RetryConfig { + public static class RetryConfig implements HasRouteId { + + private String routeId; private int retries = 3; @@ -219,6 +233,16 @@ public class RetryGatewayFilterFactory Assert.notEmpty(this.methods, "methods may not be empty"); } + @Override + public void setRouteId(String routeId) { + this.routeId = routeId; + } + + @Override + public String getRouteId() { + return this.routeId; + } + public int getRetries() { return retries; } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java index 3b6d26e0..5f8b2400 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java @@ -147,7 +147,7 @@ public class RouteDefinitionRouteLocator .replaceFilters(gatewayFilters).build(); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "Duplicates", "unchecked" }) private List loadGatewayFilters(String id, List filterDefinitions) { List filters = filterDefinitions.stream().map(definition -> { @@ -173,7 +173,7 @@ public class RouteDefinitionRouteLocator factory.shortcutFieldPrefix(), definition.getName(), validator, conversionService); - GatewayFilter gatewayFilter = factory.apply(configuration); + GatewayFilter gatewayFilter = factory.apply(id, configuration); if (this.publisher != null) { this.publisher.publishEvent(new FilterArgsEvent(this, id, properties)); } 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 3a72b1aa..1fa2093f 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 @@ -474,8 +474,9 @@ public class GatewayFilterSpec extends UriSpec { * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec retry(int retries) { - return filter(getBean(RetryGatewayFilterFactory.class) - .apply(retryConfig -> retryConfig.setRetries(retries))); + return filter( + getBean(RetryGatewayFilterFactory.class).apply(this.routeBuilder.getId(), + retryConfig -> retryConfig.setRetries(retries))); } /** @@ -487,7 +488,8 @@ public class GatewayFilterSpec extends UriSpec { */ public GatewayFilterSpec retry( Consumer retryConsumer) { - return filter(getBean(RetryGatewayFilterFactory.class).apply(retryConsumer)); + return filter(getBean(RetryGatewayFilterFactory.class) + .apply(this.routeBuilder.getId(), retryConsumer)); } /** @@ -498,7 +500,9 @@ public class GatewayFilterSpec extends UriSpec { */ public GatewayFilterSpec retry(Repeat repeat, Retry retry) { - return filter(getBean(RetryGatewayFilterFactory.class).apply(repeat, retry)); + RetryGatewayFilterFactory filterFactory = getBean( + RetryGatewayFilterFactory.class); + return filter(filterFactory.apply(this.routeBuilder.getId(), repeat, retry)); } /** diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/HasRouteId.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/HasRouteId.java new file mode 100644 index 00000000..a9db1637 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/HasRouteId.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2019 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 + * + * https://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.support; + +public interface HasRouteId { + + void setRouteId(String routeId); + + String getRouteId(); + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index e0459e28..9df89ac2 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -37,7 +37,8 @@ public class GatewayAutoConfigurationTests { @Test public void noHiddenHttpMethodFilter() { try (ConfigurableApplicationContext ctx = SpringApplication.run( - NoHiddenHttpMethodFilterConfig.class, "--spring.jmx.enabled=false")) { + NoHiddenHttpMethodFilterConfig.class, "--spring.jmx.enabled=false", + "--server.port=0")) { assertThat(ctx.getEnvironment() .getProperty("spring.webflux.hiddenmethod.filter.enabled")) .isEqualTo("false"); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactoryTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactoryTest.java index 011ea152..dc8b460c 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactoryTest.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RequestSizeGatewayFilterFactoryTest.java @@ -43,14 +43,14 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen @DirtiesContext public class RequestSizeGatewayFilterFactoryTest extends BaseWebClientTests { - private static final String responseMesssage = "Request size is larger than permissible limit. Request size is 6.0 MB " - + "where permissible limit is 5.0 MB"; + private static final String responseMesssage = "Request size is larger than permissible limit. Request size is . . " + + "where permissible limit is .*"; @Test public void setRequestSizeFilterWorks() { - testClient.get().uri("/headers").header("Host", "www.setrequestsize.org") - .header("content-length", "6000000").exchange().expectStatus() - .isEqualTo(HttpStatus.PAYLOAD_TOO_LARGE).expectHeader() + testClient.post().uri("/post").header("Host", "www.setrequestsize.org") + .header("content-length", "6").syncBody("123456").exchange() + .expectStatus().isEqualTo(HttpStatus.PAYLOAD_TOO_LARGE).expectHeader() .valueMatches("errorMessage", responseMesssage); } @@ -67,7 +67,7 @@ public class RequestSizeGatewayFilterFactoryTest extends BaseWebClientTests { return builder.routes() .route("test_request_size", r -> r.order(-1).host("**.setrequestsize.org") - .filters(f -> f.setRequestSize(5000000L)).uri(uri)) + .filters(f -> f.setRequestSize(5L)).uri(uri)) .build(); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java index 70003dee..7cfa6f4d 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java @@ -40,6 +40,7 @@ import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; @@ -79,11 +80,10 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest } @Test - // TODO: support post public void retryFilterPost() { - testClient.post().uri("/retry?key=post").exchange().expectStatus() - .is5xxServerError(); - // .expectBody(String.class).isEqualTo("3"); + testClient.post().uri("/retry?key=post") + .header(HttpHeaders.HOST, "www.retryjava.org").syncBody("Hello") + .exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3"); } @Test @@ -145,7 +145,8 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest return builder.routes() .route("retry_java", r -> r.host("**.retryjava.org") .filters(f -> f.prefixPath("/httpbin") - .retry(config -> config.setRetries(2))) + .retry(config -> config.setRetries(2) + .setMethods(HttpMethod.POST, HttpMethod.GET))) .uri(uri)) .route("retry_with_loadbalancer", r -> r.host("**.retrywithloadbalancer.org") diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java index fee866e9..fbbc20c7 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/FormIntegrationTests.java @@ -21,17 +21,18 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.LinkedMultiValueMap; @@ -42,7 +43,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.cloud.gateway.test.TestUtils.getMap; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; -import static org.springframework.web.reactive.function.BodyExtractors.toMono; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -73,6 +73,8 @@ public class FormIntegrationTests extends BaseWebClientTests { public void multipartFormDataWorks() { ClassPathResource img = new ClassPathResource("1x1.png"); + TestRestTemplate rest = new TestRestTemplate(); + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.IMAGE_PNG); @@ -81,17 +83,14 @@ public class FormIntegrationTests extends BaseWebClientTests { MultiValueMap parts = new LinkedMultiValueMap<>(); parts.add("imgpart", entity); - Mono result = webClient.post().uri("/post") - .contentType(MediaType.MULTIPART_FORM_DATA) - .body(BodyInserters.fromMultipartData(parts)).exchange() - .flatMap(response -> response.body(toMono(Map.class))); + ResponseEntity response = rest.postForEntity(baseUri + "/post", parts, + Map.class); - StepVerifier.create(result).consumeNextWith(map -> { - Map files = getMap(map, "files"); - assertThat(files).containsKey("imgpart"); - String file = (String) files.get("imgpart"); - assertThat(file).startsWith("data:").contains(";base64,"); - }).expectComplete().verify(DURATION); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + Map files = getMap(response.getBody(), "files"); + assertThat(files).containsKey("imgpart"); + String file = (String) files.get("imgpart"); + assertThat(file).startsWith("data:").contains(";base64,"); } @EnableAutoConfiguration diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 7a7c4c98..8b596dc6 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -6,7 +6,7 @@ spring-cloud-dependencies-parent org.springframework.cloud - 2.1.4.BUILD-SNAPSHOT + 2.1.6.BUILD-SNAPSHOT