Merge branch 'filter/local-response-cache/no-cache-directive' into 4.0.x
This commit is contained in:
@@ -65,7 +65,7 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
@Qualifier(RESPONSE_CACHE_MANAGER_NAME) CacheManager cacheManager,
|
||||
LocalResponseCacheProperties properties) {
|
||||
return new GlobalLocalResponseCacheGatewayFilter(responseCacheManagerFactory, responseCache(cacheManager),
|
||||
properties.getTimeToLive());
|
||||
properties.getTimeToLive(), properties.getRequest());
|
||||
}
|
||||
|
||||
@Bean(name = RESPONSE_CACHE_MANAGER_NAME)
|
||||
@@ -78,7 +78,7 @@ public class LocalResponseCacheAutoConfiguration {
|
||||
public LocalResponseCacheGatewayFilterFactory localResponseCacheGatewayFilterFactory(
|
||||
ResponseCacheManagerFactory responseCacheManagerFactory, LocalResponseCacheProperties properties) {
|
||||
return new LocalResponseCacheGatewayFilterFactory(responseCacheManagerFactory, properties.getTimeToLive(),
|
||||
properties.getSize());
|
||||
properties.getSize(), properties.getRequest());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -40,12 +40,19 @@ public class GlobalLocalResponseCacheGatewayFilter implements GlobalFilter, Orde
|
||||
|
||||
private final ResponseCacheGatewayFilter responseCacheGatewayFilter;
|
||||
|
||||
@Deprecated
|
||||
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
|
||||
Duration configuredTimeToLive) {
|
||||
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
|
||||
cacheManagerFactory.create(globalCache, configuredTimeToLive));
|
||||
}
|
||||
|
||||
public GlobalLocalResponseCacheGatewayFilter(ResponseCacheManagerFactory cacheManagerFactory, Cache globalCache,
|
||||
Duration configuredTimeToLive, LocalResponseCacheProperties.RequestOptions requestOptions) {
|
||||
responseCacheGatewayFilter = new ResponseCacheGatewayFilter(
|
||||
cacheManagerFactory.create(globalCache, configuredTimeToLive, requestOptions));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
if (exchange.getAttributes().get(LOCAL_RESPONSE_CACHE_FILTER_APPLIED) == null) {
|
||||
|
||||
@@ -24,17 +24,16 @@ import org.springframework.cache.Cache;
|
||||
import org.springframework.cloud.gateway.config.LocalResponseCacheAutoConfiguration;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.RequestOptions;
|
||||
import org.springframework.cloud.gateway.support.HasRouteId;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory} of
|
||||
* {@link ResponseCacheGatewayFilter}.
|
||||
*
|
||||
* By default, a global cache (defined as properties in the application) is used. For
|
||||
* specific route configuration, parameters can be added following
|
||||
* {@link RouteCacheConfiguration} class.
|
||||
* {@link ResponseCacheGatewayFilter}. By default, a global cache (defined as properties
|
||||
* in the application) is used. For specific route configuration, parameters can be added
|
||||
* following {@link RouteCacheConfiguration} class.
|
||||
*
|
||||
* @author Marta Medio
|
||||
* @author Ignacio Lozano
|
||||
@@ -49,23 +48,27 @@ public class LocalResponseCacheGatewayFilterFactory
|
||||
*/
|
||||
public static final String LOCAL_RESPONSE_CACHE_FILTER_APPLIED = "LocalResponseCacheGatewayFilter-Applied";
|
||||
|
||||
private ResponseCacheManagerFactory cacheManagerFactory;
|
||||
private final ResponseCacheManagerFactory cacheManagerFactory;
|
||||
|
||||
private Duration defaultTimeToLive;
|
||||
private final Duration defaultTimeToLive;
|
||||
|
||||
private DataSize defaultSize;
|
||||
private final DataSize defaultSize;
|
||||
|
||||
private final RequestOptions requestOptions;
|
||||
|
||||
@Deprecated
|
||||
public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
|
||||
Duration defaultTimeToLive) {
|
||||
this(cacheManagerFactory, defaultTimeToLive, null);
|
||||
Duration defaultTimeToLive, DataSize defaultSize) {
|
||||
this(cacheManagerFactory, defaultTimeToLive, defaultSize, new RequestOptions());
|
||||
}
|
||||
|
||||
public LocalResponseCacheGatewayFilterFactory(ResponseCacheManagerFactory cacheManagerFactory,
|
||||
Duration defaultTimeToLive, DataSize defaultSize) {
|
||||
Duration defaultTimeToLive, DataSize defaultSize, RequestOptions requestOptions) {
|
||||
super(RouteCacheConfiguration.class);
|
||||
this.cacheManagerFactory = cacheManagerFactory;
|
||||
this.defaultTimeToLive = defaultTimeToLive;
|
||||
this.defaultSize = defaultSize;
|
||||
this.requestOptions = requestOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,7 +77,8 @@ public class LocalResponseCacheGatewayFilterFactory
|
||||
|
||||
Cache routeCache = LocalResponseCacheAutoConfiguration.createGatewayCacheManager(cacheProperties)
|
||||
.getCache(config.getRouteId() + "-cache");
|
||||
return new ResponseCacheGatewayFilter(cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive()));
|
||||
return new ResponseCacheGatewayFilter(
|
||||
cacheManagerFactory.create(routeCache, cacheProperties.getTimeToLive(), requestOptions));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ public class LocalResponseCacheProperties {
|
||||
|
||||
private Duration timeToLive;
|
||||
|
||||
private RequestOptions request = new RequestOptions();
|
||||
|
||||
public DataSize getSize() {
|
||||
return size;
|
||||
}
|
||||
@@ -64,9 +66,57 @@ public class LocalResponseCacheProperties {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
public RequestOptions getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public void setRequest(RequestOptions request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LocalResponseCacheProperties{" + "timeToLive=" + getTimeToLive() + '\'' + ", size='" + getSize() + '}';
|
||||
return "LocalResponseCacheProperties{" + "size=" + size + ", timeToLive=" + timeToLive + ", request=" + request
|
||||
+ '}';
|
||||
}
|
||||
|
||||
public static class RequestOptions {
|
||||
|
||||
private NoCacheStrategy noCacheStrategy = NoCacheStrategy.SKIP_UPDATE_CACHE_ENTRY;
|
||||
|
||||
public NoCacheStrategy getNoCacheStrategy() {
|
||||
return noCacheStrategy;
|
||||
}
|
||||
|
||||
public void setNoCacheStrategy(NoCacheStrategy noCacheStrategy) {
|
||||
this.noCacheStrategy = noCacheStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RequestOptions{" + "noCacheStrategy=" + noCacheStrategy + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When client sends "no-cache" directive in "Cache-Control" header, the response
|
||||
* should be re-validated from upstream. There are several strategies that indicates
|
||||
* what to do with the new fresh response.
|
||||
*/
|
||||
public enum NoCacheStrategy {
|
||||
|
||||
/**
|
||||
* Update the cache entry by the fresh response coming from upstream with a new
|
||||
* time to live.
|
||||
*/
|
||||
UPDATE_CACHE_ENTRY,
|
||||
/**
|
||||
* Skip the update. The client will receive the fresh response, other clients will
|
||||
* receive the old entry in cache.
|
||||
*/
|
||||
SKIP_UPDATE_CACHE_ENTRY
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.factory.cache;
|
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public final class LocalResponseCacheUtils {
|
||||
|
||||
private LocalResponseCacheUtils() {
|
||||
}
|
||||
|
||||
public static boolean isNoCacheRequest(ServerHttpRequest request) {
|
||||
String cacheControl = request.getHeaders().getCacheControl();
|
||||
return StringUtils.hasText(cacheControl) && cacheControl.matches(".*(\s|,|^)no-cache(\\s|,|$).*");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
|
||||
|
||||
private Mono<Void> filterWithCache(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
final String metadataKey = responseCacheManager.resolveMetadataKey(exchange);
|
||||
Optional<CachedResponse> cached = responseCacheManager.getFromCache(exchange.getRequest(), metadataKey);
|
||||
Optional<CachedResponse> cached = getCachedResponse(exchange, metadataKey);
|
||||
|
||||
if (cached.isPresent()) {
|
||||
return responseCacheManager.processFromCache(exchange, metadataKey, cached.get());
|
||||
@@ -77,6 +77,22 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<CachedResponse> getCachedResponse(ServerWebExchange exchange, String metadataKey) {
|
||||
Optional<CachedResponse> cached;
|
||||
if (shouldRevalidate(exchange)) {
|
||||
cached = Optional.empty();
|
||||
}
|
||||
else {
|
||||
cached = responseCacheManager.getFromCache(exchange.getRequest(), metadataKey);
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
private boolean shouldRevalidate(ServerWebExchange exchange) {
|
||||
return LocalResponseCacheUtils.isNoCacheRequest(exchange.getRequest());
|
||||
}
|
||||
|
||||
private class CachingResponseDecorator extends ServerHttpResponseDecorator {
|
||||
|
||||
private final String metadataKey;
|
||||
@@ -94,7 +110,8 @@ public class ResponseCacheGatewayFilter implements GatewayFilter, Ordered {
|
||||
final ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
Flux<DataBuffer> decoratedBody;
|
||||
if (responseCacheManager.isResponseCacheable(response)) {
|
||||
if (responseCacheManager.isResponseCacheable(response)
|
||||
&& !responseCacheManager.isNoCacheRequestWithoutUpdate(exchange.getRequest())) {
|
||||
decoratedBody = responseCacheManager.processFromUpstream(metadataKey, exchange, Flux.from(body));
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -30,8 +30,11 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.NoCacheStrategy;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheProperties.RequestOptions;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.keygenerator.CacheKeyGenerator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.AfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetMaxAgeHeaderAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetResponseHeadersAfterCacheExchangeMutator;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.postprocessor.SetStatusCodeAfterCacheExchangeMutator;
|
||||
@@ -63,12 +66,28 @@ public class ResponseCacheManager {
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
private final boolean ignoreNoCacheUpdate;
|
||||
|
||||
@Deprecated
|
||||
public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Duration configuredTimeToLive) {
|
||||
this(cacheKeyGenerator, cache, configuredTimeToLive, new RequestOptions());
|
||||
}
|
||||
|
||||
public ResponseCacheManager(CacheKeyGenerator cacheKeyGenerator, Cache cache, Duration configuredTimeToLive,
|
||||
RequestOptions requestOptions) {
|
||||
this.cacheKeyGenerator = cacheKeyGenerator;
|
||||
this.cache = cache;
|
||||
this.ignoreNoCacheUpdate = isSkipNoCacheUpdateActive(requestOptions);
|
||||
this.afterCacheExchangeMutators = List.of(new SetResponseHeadersAfterCacheExchangeMutator(),
|
||||
new SetStatusCodeAfterCacheExchangeMutator(),
|
||||
new SetMaxAgeHeaderAfterCacheExchangeMutator(configuredTimeToLive, Clock.systemDefaultZone()));
|
||||
new SetMaxAgeHeaderAfterCacheExchangeMutator(configuredTimeToLive, Clock.systemDefaultZone(),
|
||||
ignoreNoCacheUpdate),
|
||||
new SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator());
|
||||
}
|
||||
|
||||
private static boolean isSkipNoCacheUpdateActive(RequestOptions requestOptions) {
|
||||
return requestOptions != null
|
||||
&& requestOptions.getNoCacheStrategy().equals(NoCacheStrategy.SKIP_UPDATE_CACHE_ENTRY);
|
||||
}
|
||||
|
||||
private static final List<HttpStatusCode> statusesToCache = Arrays.asList(HttpStatus.OK, HttpStatus.PARTIAL_CONTENT,
|
||||
@@ -132,13 +151,8 @@ public class ResponseCacheManager {
|
||||
afterCacheExchangeMutators.forEach(processor -> processor.accept(exchange, cachedResponse));
|
||||
saveMetadataInCache(metadataKey, new CachedResponseMetadata(cachedResponse.headers().getVary()));
|
||||
|
||||
if (HttpStatus.NOT_MODIFIED.equals(response.getStatusCode())) {
|
||||
return response.writeWith(Mono.empty());
|
||||
}
|
||||
else {
|
||||
return response.writeWith(
|
||||
Flux.fromIterable(cachedResponse.body()).map(data -> response.bufferFactory().wrap(data)));
|
||||
}
|
||||
return response
|
||||
.writeWith(Flux.fromIterable(cachedResponse.body()).map(data -> response.bufferFactory().wrap(data)));
|
||||
}
|
||||
|
||||
private CachedResponseMetadata retrieveMetadata(String metadataKey) {
|
||||
@@ -157,6 +171,10 @@ public class ResponseCacheManager {
|
||||
return isStatusCodeToCache(response) && isCacheControlAllowed(response) && !isVaryWildcard(response);
|
||||
}
|
||||
|
||||
boolean isNoCacheRequestWithoutUpdate(ServerHttpRequest request) {
|
||||
return LocalResponseCacheUtils.isNoCacheRequest(request) && ignoreNoCacheUpdate;
|
||||
}
|
||||
|
||||
private boolean isStatusCodeToCache(ServerHttpResponse response) {
|
||||
return statusesToCache.contains(response.getStatusCode());
|
||||
}
|
||||
|
||||
@@ -33,8 +33,14 @@ public class ResponseCacheManagerFactory {
|
||||
this.cacheKeyGenerator = cacheKeyGenerator;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public ResponseCacheManager create(Cache cache, Duration timeToLive) {
|
||||
return new ResponseCacheManager(cacheKeyGenerator, cache, timeToLive);
|
||||
}
|
||||
|
||||
public ResponseCacheManager create(Cache cache, Duration timeToLive,
|
||||
LocalResponseCacheProperties.RequestOptions requestOptions) {
|
||||
return new ResponseCacheManager(cacheKeyGenerator, cache, timeToLive, requestOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.factory.cache.postprocessor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
public class SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator implements AfterCacheExchangeMutator {
|
||||
|
||||
final Pattern MAX_AGE_PATTERN = Pattern.compile("(?:,|^)\\s*max-age=(\\d+)");
|
||||
|
||||
@Override
|
||||
public void accept(ServerWebExchange exchange, CachedResponse cachedResponse) {
|
||||
Optional<Integer> maxAge = Optional.ofNullable(exchange.getResponse().getHeaders().getCacheControl())
|
||||
.map(MAX_AGE_PATTERN::matcher).filter(Matcher::find).map(matcher -> matcher.group(1))
|
||||
.map(Integer::parseInt);
|
||||
|
||||
if (maxAge.isPresent()) {
|
||||
if (maxAge.get() > 0) {
|
||||
removeNoCacheHeaders(exchange);
|
||||
}
|
||||
else {
|
||||
keepNoCacheHeaders(exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void keepNoCacheHeaders(ServerWebExchange exchange) {
|
||||
// at least it contains 'max-age' so we can append items with commas safely
|
||||
String cacheControl = exchange.getResponse().getHeaders().getCacheControl();
|
||||
StringBuilder newCacheControl = new StringBuilder(cacheControl);
|
||||
|
||||
if (!cacheControl.contains("no-cache")) {
|
||||
newCacheControl.append(",no-cache");
|
||||
}
|
||||
|
||||
if (!cacheControl.contains("must-revalidate")) {
|
||||
newCacheControl.append(",must-revalidate");
|
||||
}
|
||||
exchange.getResponse().getHeaders().setCacheControl(newCacheControl.toString());
|
||||
}
|
||||
|
||||
private void removeNoCacheHeaders(ServerWebExchange exchange) {
|
||||
String cacheControl = exchange.getResponse().getHeaders().getCacheControl();
|
||||
List<String> cacheControlValues = Arrays.asList(cacheControl.split("\\s*,\\s*"));
|
||||
|
||||
String newCacheControl = cacheControlValues.stream()
|
||||
.filter(s -> !s.matches("must-revalidate|no-cache|no-store")).collect(Collectors.joining(","));
|
||||
exchange.getResponse().getHeaders().setCacheControl(newCacheControl);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,7 +25,9 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.LocalResponseCacheUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@@ -45,25 +47,33 @@ public class SetMaxAgeHeaderAfterCacheExchangeMutator implements AfterCacheExcha
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
public SetMaxAgeHeaderAfterCacheExchangeMutator(Duration configuredTimeToLive, Clock clock) {
|
||||
private final boolean ignoreNoCacheUpdate;
|
||||
|
||||
public SetMaxAgeHeaderAfterCacheExchangeMutator(Duration configuredTimeToLive, Clock clock,
|
||||
boolean ignoreNoCacheUpdate) {
|
||||
this.configuredTimeToLive = configuredTimeToLive;
|
||||
this.clock = clock;
|
||||
this.ignoreNoCacheUpdate = ignoreNoCacheUpdate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(ServerWebExchange exchange, CachedResponse cachedResponse) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
long calculatedMaxAgeInSeconds = calculateMaxAgeInSeconds(cachedResponse, configuredTimeToLive);
|
||||
long calculatedMaxAgeInSeconds = calculateMaxAgeInSeconds(exchange.getRequest(), cachedResponse,
|
||||
configuredTimeToLive);
|
||||
rewriteCacheControlMaxAge(response.getHeaders(), calculatedMaxAgeInSeconds);
|
||||
}
|
||||
|
||||
private long calculateMaxAgeInSeconds(CachedResponse cachedResponse, Duration configuredTimeToLive) {
|
||||
private long calculateMaxAgeInSeconds(ServerHttpRequest request, CachedResponse cachedResponse,
|
||||
Duration configuredTimeToLive) {
|
||||
boolean noCache = LocalResponseCacheUtils.isNoCacheRequest(request);
|
||||
long maxAge;
|
||||
if (configuredTimeToLive.getSeconds() == -1) {
|
||||
maxAge = -1;
|
||||
if (noCache && ignoreNoCacheUpdate || configuredTimeToLive.getSeconds() < 0) {
|
||||
maxAge = 0;
|
||||
}
|
||||
else {
|
||||
maxAge = Math.max(0, configuredTimeToLive.minus(getElapsedTimeInSeconds(cachedResponse)).getSeconds());
|
||||
long calculatedMaxAge = configuredTimeToLive.minus(getElapsedTimeInSeconds(cachedResponse)).getSeconds();
|
||||
maxAge = Math.max(0, calculatedMaxAge);
|
||||
}
|
||||
|
||||
return maxAge;
|
||||
@@ -77,10 +87,10 @@ public class SetMaxAgeHeaderAfterCacheExchangeMutator implements AfterCacheExcha
|
||||
boolean isMaxAgePresent = headers.getCacheControl() != null
|
||||
&& headers.getCacheControl().contains(MAX_AGE_PREFIX);
|
||||
|
||||
List<String> newCacheControlDirectives = new ArrayList<>();
|
||||
if (isMaxAgePresent) {
|
||||
List<String> cacheControlHeaders = headers.get(HttpHeaders.CACHE_CONTROL);
|
||||
cacheControlHeaders = cacheControlHeaders == null ? Collections.emptyList() : cacheControlHeaders;
|
||||
List<String> replacedCacheControlHeaders = new ArrayList<>();
|
||||
for (String value : cacheControlHeaders) {
|
||||
if (value.contains(MAX_AGE_PREFIX)) {
|
||||
if (seconds == -1) {
|
||||
@@ -92,11 +102,17 @@ public class SetMaxAgeHeaderAfterCacheExchangeMutator implements AfterCacheExcha
|
||||
value = value.replaceFirst("\\bmax-age=\\d+\\b", MAX_AGE_PREFIX + seconds);
|
||||
}
|
||||
}
|
||||
replacedCacheControlHeaders.add(value);
|
||||
newCacheControlDirectives.add(value);
|
||||
}
|
||||
headers.remove(HttpHeaders.CACHE_CONTROL);
|
||||
headers.addAll(HttpHeaders.CACHE_CONTROL, replacedCacheControlHeaders);
|
||||
}
|
||||
else {
|
||||
List<String> cacheControlHeaders = headers.get(HttpHeaders.CACHE_CONTROL);
|
||||
newCacheControlDirectives = cacheControlHeaders == null ? new ArrayList<>()
|
||||
: new ArrayList<>(cacheControlHeaders);
|
||||
newCacheControlDirectives.add("max-age=" + seconds);
|
||||
}
|
||||
headers.remove(HttpHeaders.CACHE_CONTROL);
|
||||
headers.addAll(HttpHeaders.CACHE_CONTROL, newCacheControlDirectives);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,38 +17,21 @@
|
||||
package org.springframework.cloud.gateway.filter.factory.cache.postprocessor;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* It sets HTTP Status Code depending {@literal no-cache}
|
||||
* {@link HttpHeaders#CACHE_CONTROL} header.
|
||||
* It sets HTTP Status Code.
|
||||
*
|
||||
* @author Marta Medio
|
||||
* @author Ignacio Lozano
|
||||
*/
|
||||
public class SetStatusCodeAfterCacheExchangeMutator implements AfterCacheExchangeMutator {
|
||||
|
||||
private static final String NO_CACHE_VALUE = "no-cache";
|
||||
|
||||
@Override
|
||||
public void accept(ServerWebExchange exchange, CachedResponse cachedResponse) {
|
||||
HttpHeaders requestHeaders = exchange.getRequest().getHeaders();
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
if (!CollectionUtils.isEmpty(cachedResponse.body()) && isRequestNoCache(requestHeaders)) {
|
||||
response.setStatusCode(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
else {
|
||||
response.setStatusCode(cachedResponse.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRequestNoCache(HttpHeaders requestHeaders) {
|
||||
return requestHeaders.getCacheControl() != null && requestHeaders.getCacheControl().contains(NO_CACHE_VALUE);
|
||||
response.setStatusCode(cachedResponse.statusCode());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,13 +45,13 @@
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.local-response-cache.size",
|
||||
"type": "org.springframework.util.unit.DataSize",
|
||||
"description": "Maximum size of the cache to evict entries for this route (in KB, MB and GB).",
|
||||
"defaultValue": "5m"
|
||||
"description": "Maximum size of the cache to evict entries for this route (in KB, MB and GB)."
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.local-response-cache.time-to-live",
|
||||
"type": "java.time.Duration",
|
||||
"description": "Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours)."
|
||||
"description": "Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).",
|
||||
"defaultValue": "5m"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.dedupe-response-header.enabled",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.cloud.gateway.filter.factory.cache;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.data.Offset.offset;
|
||||
|
||||
/**
|
||||
* @author Ignacio Lozano
|
||||
@@ -53,12 +55,23 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ActiveProfiles(profiles = "local-cache-filter")
|
||||
public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTests {
|
||||
|
||||
private static final String CUSTOM_HEADER = "X-Custom-Date";
|
||||
private static final String CUSTOM_HEADER = "X-Custom-Header";
|
||||
|
||||
private static Long parseMaxAge(String cacheControlValue) {
|
||||
if (StringUtils.hasText(cacheControlValue)) {
|
||||
Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
|
||||
Matcher matcher = maxAgePattern.matcher(cacheControlValue);
|
||||
if (matcher.find()) {
|
||||
return Long.parseLong(matcher.group(1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true" },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class LocalResponseCacheUsingFilterParams extends BaseWebClientTests {
|
||||
public class UsingFilterParams extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
void shouldNotCacheResponseWhenGetRequestHasBody() {
|
||||
@@ -101,17 +114,18 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCacheAndReturnNotModifiedStatusWhenCacheControlIsNoCache() {
|
||||
void shouldNotIncludeMustRevalidateNoStoreAndNoCacheDirectivesWhenMaxAgeIsPositive() {
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "1").exchange()
|
||||
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
|
||||
var response = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult();
|
||||
var maxAge = response.getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2")
|
||||
// Cache-Control asks to not return cached content because it is
|
||||
// HttpHeaders.NotModified
|
||||
.header(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue()).exchange()
|
||||
.expectStatus().isNotModified().expectBody().isEmpty();
|
||||
assertThat(maxAge).isGreaterThan(0L);
|
||||
assertThat(response.getResponseHeaders().get(HttpHeaders.CACHE_CONTROL)).doesNotContain("no-store",
|
||||
"must-revalidate", "no-cache");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -163,11 +177,13 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
Thread.sleep(2000);
|
||||
Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
|
||||
assertThat(maxAgeRequest2).isLessThan(maxAgeRequest1);
|
||||
}
|
||||
@@ -213,17 +229,6 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
.jsonPath("$.headers." + CUSTOM_HEADER, "2");
|
||||
}
|
||||
|
||||
private Long parseMaxAge(String cacheControlValue) {
|
||||
if (StringUtils.hasText(cacheControlValue)) {
|
||||
Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
|
||||
Matcher matcher = maxAgePattern.matcher(cacheControlValue);
|
||||
if (matcher.find()) {
|
||||
return Long.parseLong(matcher.group(1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void assertNonVaryHeaderInContent(String uri, String varyHeader, String varyHeaderValue, String nonVaryHeader,
|
||||
String nonVaryHeaderValue, String expectedNonVaryResponse) {
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header("X-Request-Vary", varyHeader)
|
||||
@@ -275,34 +280,185 @@ public class LocalResponseCacheGatewayFilterFactoryTests extends BaseWebClientTe
|
||||
properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true",
|
||||
"spring.cloud.gateway.filter.local-response-cache.timeToLive=20s" },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class LocalResponseCacheUsingDefaultProperties extends BaseWebClientTests {
|
||||
public class UsingPropertiesAsDefault extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
void shouldApplyMaxAgeFromPropertiesWhenFilterHasNoParams() throws InterruptedException {
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAgeRequest1).isLessThanOrEqualTo(20L);
|
||||
|
||||
Thread.sleep(2000);
|
||||
|
||||
Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(this::parseMaxAge).filter(Objects::nonNull).findAny().orElse(null);
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
|
||||
assertThat(maxAgeRequest2).isLessThan(maxAgeRequest1);
|
||||
}
|
||||
|
||||
private Long parseMaxAge(String cacheControlValue) {
|
||||
if (StringUtils.hasText(cacheControlValue)) {
|
||||
Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
|
||||
Matcher matcher = maxAgePattern.matcher(cacheControlValue);
|
||||
if (matcher.find()) {
|
||||
return Long.parseLong(matcher.group(1));
|
||||
}
|
||||
@Test
|
||||
void shouldNotCacheWhenPrivateDirectiveIsInRequest() {
|
||||
testClient = testClient.mutate().responseTimeout(Duration.ofHours(1)).build();
|
||||
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
|
||||
.header(HttpHeaders.CACHE_CONTROL, CacheControl.noStore().getHeaderValue())
|
||||
.header(CUSTOM_HEADER, "1").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER);
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "2").exchange()
|
||||
.expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
|
||||
|
||||
testClient.get().uri(uri).header("Host", "www.localresponsecache.org").header(CUSTOM_HEADER, "3") // second
|
||||
// request
|
||||
// cached
|
||||
// "2"
|
||||
// ->
|
||||
// "3"
|
||||
// will
|
||||
// be
|
||||
// ignored
|
||||
.exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2");
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
String uri;
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes().route("local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/cache/**").and().host("{sub}.localresponsecache.org")
|
||||
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null, null))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true",
|
||||
"spring.cloud.gateway.filter.local-response-cache.time-to-live=2m",
|
||||
"spring.cloud.gateway.filter.local-response-cache.request.no-cache-strategy=skip-update-cache-entry" },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class DirectiveNoCacheSkippingUpdate extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
void shouldNotCacheWhenCacheControlAsksToValidateWithNotCache_refreshCacheWhenDirectiveNoCache()
|
||||
throws InterruptedException {
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
|
||||
// 1. Store in cache - max-age ~= 2m AND NOT
|
||||
// (must-revalidate,no-cache,no-store)
|
||||
final Instant when1stRequest = Instant.now();
|
||||
var firstResponse = testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
|
||||
.header(CUSTOM_HEADER, "1").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER)
|
||||
.isEqualTo("1").returnResult();
|
||||
var maxAge1st = firstResponse.getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAge1st).isCloseTo(Duration.ofMinutes(2).toSeconds(), offset(10L));
|
||||
assertThat(firstResponse.getResponseHeaders().getCacheControl()).doesNotContain("must-revalidate",
|
||||
"no-cache", "no-store");
|
||||
|
||||
// 2. "no-cache" should return max-age=0 & must-revalidate,no-cache,no-store
|
||||
var secondResponse = testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
|
||||
.header(CUSTOM_HEADER, "2")
|
||||
// Cache-Control asks to not use the cached content
|
||||
.header(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue()).exchange().expectBody()
|
||||
.jsonPath("$.headers." + CUSTOM_HEADER).isEqualTo("2").returnResult();
|
||||
var maxAge2nd = secondResponse.getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAge2nd).isZero();
|
||||
|
||||
// 3. After 2s, max-age = (when1stRequest) - 1s - offset_delay
|
||||
var waitDuration = Duration.ofSeconds(1);
|
||||
Thread.sleep(waitDuration.toMillis()); // Wait 2s to check max-age renewed
|
||||
final Instant when3rdRequest = Instant.now();
|
||||
var thirdResponseCached = testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
|
||||
.header(CUSTOM_HEADER, "3").exchange().expectBody().jsonPath("$.headers." + CUSTOM_HEADER)
|
||||
.isEqualTo("1").returnResult();
|
||||
var maxAge3rd = thirdResponseCached.getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAge3rd).isCloseTo(
|
||||
Duration.ofMinutes(2).minus(Duration.between(when1stRequest, when3rdRequest)).getSeconds(),
|
||||
offset(10L));
|
||||
assertThat(maxAge3rd).isNotZero();
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
String uri;
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes().route("local_response_cache_java_test",
|
||||
r -> r.path("/{namespace}/cache/**").and().host("{sub}.localresponsecache.org")
|
||||
.filters(f -> f.stripPrefix(2).prefixPath("/httpbin").localResponseCache(null, null))
|
||||
.uri(uri))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(
|
||||
properties = { "spring.cloud.gateway.filter.local-response-cache.enabled=true",
|
||||
"spring.cloud.gateway.filter.local-response-cache.time-to-live=2m",
|
||||
"spring.cloud.gateway.filter.local-response-cache.request.no-cache-strategy=update-cache-entry" },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class DirectiveNoCacheWithUpdate extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
void oldMaxAgeWhenNoCacheRequest() throws InterruptedException {
|
||||
testClient = testClient.mutate().responseTimeout(Duration.ofHours(1)).build();
|
||||
|
||||
String uri = "/" + UUID.randomUUID() + "/cache/headers";
|
||||
// First request -> cache miss
|
||||
final Instant when1stRequest = Instant.now();
|
||||
Long maxAgeRequest1 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAgeRequest1).isCloseTo(Duration.ofMinutes(2).toSeconds(), offset(10L));
|
||||
|
||||
// Second request + no-cache -> skip cache and ignore update
|
||||
Thread.sleep(1000);
|
||||
final Duration between1stAnd2ndRequest = Duration.between(when1stRequest, Instant.now());
|
||||
Long maxAgeRequest2 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org")
|
||||
.header(HttpHeaders.CACHE_CONTROL, CacheControl.noCache().getHeaderValue()).exchange().expectBody()
|
||||
.returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAgeRequest2).isCloseTo(Duration.ofMinutes(2).minus(between1stAnd2ndRequest).getSeconds(),
|
||||
offset(10L));
|
||||
|
||||
// Third request -> cache hit -> entry (and max-age) is updated
|
||||
Thread.sleep(1000);
|
||||
Long maxAgeRequest3 = testClient.get().uri(uri).header("Host", "www.localresponsecache.org").exchange()
|
||||
.expectBody().returnResult().getResponseHeaders().get(HttpHeaders.CACHE_CONTROL).stream()
|
||||
.map(LocalResponseCacheGatewayFilterFactoryTests::parseMaxAge).filter(Objects::nonNull).findAny()
|
||||
.orElse(null);
|
||||
assertThat(maxAgeRequest3).isCloseTo(Duration.ofMinutes(2).toSeconds(), offset(10L));
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.factory.cache;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LocalResponseCacheUtilsTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "", "no-store", "no-store, wrong-no-cache", "s-no-cache" })
|
||||
void shouldNotIdentifyRequestAsNoCacheRequest(String cacheControl) {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this")
|
||||
.header("Cache-Control", cacheControl).build();
|
||||
|
||||
boolean result = LocalResponseCacheUtils.isNoCacheRequest(httpRequest);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "no-cache", "s-no-cache, no-cache", "private,no-cache", " no-cache", "no-cache " })
|
||||
void shouldIdentifyRequestAsNoCacheRequest(String cacheControl) {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this")
|
||||
.header("Cache-Control", cacheControl).build();
|
||||
|
||||
boolean result = LocalResponseCacheUtils.isNoCacheRequest(httpRequest);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
class ResponseCacheGatewayFilterTest {
|
||||
|
||||
ResponseCacheManager cacheManagerToTest = new ResponseCacheManager(null, null, null);
|
||||
ResponseCacheManager cacheManagerToTest = new ResponseCacheManager(null, null, null, null);
|
||||
|
||||
@Test
|
||||
void requestShouldBeCacheable() {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.factory.cache.postprocessor;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SetCacheDirectivesByMaxAgeAfterCacheExchangeMutatorTests {
|
||||
|
||||
private MockServerWebExchange inputExchange;
|
||||
|
||||
private SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator toTest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.setCacheControl("max-age=1234");
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this").build();
|
||||
|
||||
inputExchange = MockServerWebExchange.from(httpRequest);
|
||||
MockServerHttpResponse httpResponse = inputExchange.getResponse();
|
||||
httpResponse.setStatusCode(HttpStatus.OK);
|
||||
httpResponse.getHeaders().putAll(responseHeaders);
|
||||
|
||||
toTest = new SetCacheDirectivesByMaxAgeAfterCacheExchangeMutator();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "", "ETag=1234-123", "s-max-age=20" })
|
||||
void doesntModifyCacheControlWhenNoMaxAge(String cacheControlValue) {
|
||||
inputExchange.getResponse().getHeaders().setCacheControl(cacheControlValue);
|
||||
|
||||
toTest.accept(inputExchange, null);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders().getCacheControl()).isEqualTo(cacheControlValue);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = { "max-age=0", "ETag=1234-123,max-age=0", "s-max-age=20,max-age=0", "ETag=with-spaces, max-age=0",
|
||||
"ETag=with-spaces, max-age=0,Expires=123123123", " max-age=0, ETag=with-spaces" })
|
||||
void directivesNoCacheAreAddedWhenMaxAgeIsZero(String cacheControlValue) {
|
||||
inputExchange.getResponse().getHeaders().setCacheControl(cacheControlValue);
|
||||
|
||||
toTest.accept(inputExchange, null);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders().getCacheControl()).doesNotContainPattern(",\\s*,")
|
||||
.contains("max-age=0").contains("must-revalidate").contains("no-cache");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "max-age=10,must-revalidate", "must-revalidate,ETag=1234-123,max-age=10",
|
||||
"must-revalidate,s-max-age=0,max-age=10", " max-age=10, must-revalidate,ETag=with-spaces",
|
||||
"ETag=with-spaces,must-revalidate, max-age=10,Expires=123123123",
|
||||
"ETag=with-spaces,must-revalidate, max-age=10", "max-age=10,no-store" })
|
||||
void directivesNoCacheAreRemovedWhenMaxAgePositive(String cacheControlValue) {
|
||||
inputExchange.getResponse().getHeaders().setCacheControl(cacheControlValue);
|
||||
|
||||
toTest.accept(inputExchange, null);
|
||||
|
||||
assertThat(inputExchange.getResponse().getHeaders().getCacheControl()).contains("max-age=10")
|
||||
.doesNotContainPattern(",\\s*,").doesNotContain("no-store").doesNotContain("must-revalidate")
|
||||
.doesNotContain("no-cache");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -67,16 +68,29 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAgeIsNotAdded_whenMaxAgeIsNotPresent() {
|
||||
void maxAgeIsTimeToLive_whenMaxAgeIsNotPresent() {
|
||||
inputExchange.getResponse().getHeaders().setCacheControl((String) null);
|
||||
|
||||
Duration timeToLive = Duration.ofSeconds(30);
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock);
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
assertThat(parseMaxAge(inputExchange.getResponse())).isEmpty();
|
||||
assertThat(parseMaxAge(inputExchange.getResponse()).get()).isEqualTo(timeToLive.getSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxAgeIsZero_whenTimeToLiveIsNegative() {
|
||||
inputExchange.getResponse().getHeaders().setCacheControl((String) null);
|
||||
|
||||
Duration timeToLive = Duration.ofSeconds(-1);
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
assertThat(parseMaxAge(inputExchange.getResponse()).get()).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,12 +99,12 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock);
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
Optional<Long> firstMaxAgeSeconds = parseMaxAge(inputExchange.getResponse());
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTestSecondsLater = new SetMaxAgeHeaderAfterCacheExchangeMutator(
|
||||
timeToLive, clockSecondsLater);
|
||||
timeToLive, clockSecondsLater, false);
|
||||
toTestSecondsLater.accept(inputExchange, inputCachedResponse);
|
||||
Optional<Long> secondMaxAgeSeconds = parseMaxAge(inputExchange.getResponse());
|
||||
|
||||
@@ -106,12 +120,12 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock);
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
Optional<Long> firstMaxAgeSeconds = parseMaxAge(inputExchange.getResponse());
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTestSecondsLater = new SetMaxAgeHeaderAfterCacheExchangeMutator(
|
||||
timeToLive, clockSecondsLater);
|
||||
timeToLive, clockSecondsLater, false);
|
||||
toTestSecondsLater.accept(inputExchange, inputCachedResponse);
|
||||
Optional<Long> secondMaxAgeSeconds = parseMaxAge(inputExchange.getResponse());
|
||||
|
||||
@@ -126,11 +140,11 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock);
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
|
||||
String[] cacheControlValues = Optional.ofNullable(inputExchange.getResponse().getHeaders().getCacheControl())
|
||||
.map(s -> s.split("\\s*,\\s*")).orElse(null);
|
||||
String[] cacheControlValues = StringUtils
|
||||
.tokenizeToStringArray(inputExchange.getResponse().getHeaders().getCacheControl(), ",");
|
||||
assertThat(cacheControlValues).contains("max-stale=12", "min-stale=1");
|
||||
}
|
||||
|
||||
@@ -141,7 +155,7 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
CachedResponse inputCachedResponse = CachedResponse.create(HttpStatus.OK).timestamp(clock.instant()).build();
|
||||
|
||||
SetMaxAgeHeaderAfterCacheExchangeMutator toTest = new SetMaxAgeHeaderAfterCacheExchangeMutator(timeToLive,
|
||||
clock);
|
||||
clock, false);
|
||||
toTest.accept(inputExchange, inputCachedResponse);
|
||||
|
||||
List<String> cacheControlValues = inputExchange.getResponse().getHeaders().get("X-Custom-Header");
|
||||
@@ -149,18 +163,22 @@ class SetMaxAgeHeaderAfterCacheExchangeMutatorTest {
|
||||
}
|
||||
|
||||
private Optional<Long> parseMaxAge(ServerHttpResponse response) {
|
||||
return parseMaxAge(response.getHeaders().getCacheControl());
|
||||
return parseMaxAge(response.getHeaders().get("Cache-Control"));
|
||||
}
|
||||
|
||||
private Optional<Long> parseMaxAge(String cacheControlValue) {
|
||||
if (StringUtils.hasText(cacheControlValue)) {
|
||||
Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
|
||||
Matcher matcher = maxAgePattern.matcher(cacheControlValue);
|
||||
if (matcher.find()) {
|
||||
return Optional.of(Long.parseLong(matcher.group(1)));
|
||||
}
|
||||
private Optional<Long> parseMaxAge(List<String> cacheControlValues) {
|
||||
if (CollectionUtils.isEmpty(cacheControlValues)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.empty();
|
||||
|
||||
final Pattern maxAgePattern = Pattern.compile("\\bmax-age=(\\d+)\\b");
|
||||
return cacheControlValues.stream().map(cacheControlDirective -> {
|
||||
Matcher matcher = maxAgePattern.matcher(cacheControlDirective);
|
||||
if (matcher.find()) {
|
||||
return Long.parseLong(matcher.group(1));
|
||||
}
|
||||
return null;
|
||||
}).filter(maxAge -> maxAge != null).findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
package org.springframework.cloud.gateway.filter.factory.cache.postprocessor;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.factory.cache.CachedResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -47,42 +48,18 @@ class SetStatusCodeAfterCacheExchangeMutatorTest {
|
||||
httpResponse.getHeaders().putAll(responseHeaders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusCodeIs304_whenCacheHitsAndNoCacheHeaderIsPresent() {
|
||||
CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).body("some-data").build();
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this")
|
||||
.header("Cache-Control", "no-cache").build();
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { 200, 400, 404, 500 })
|
||||
void statusCodeIsSetFromCachedResponse(int statusCode) {
|
||||
CachedResponse cachedResponse = CachedResponse.create(HttpStatus.valueOf(statusCode)).body("some-data").build();
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this").build();
|
||||
|
||||
inputExchange = MockServerWebExchange.from(httpRequest);
|
||||
|
||||
SetStatusCodeAfterCacheExchangeMutator toTest = new SetStatusCodeAfterCacheExchangeMutator();
|
||||
toTest.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusCodeIs200_whenCacheHitsAndNoCacheHeaderIsNotPresent() {
|
||||
CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).body("some-data").build();
|
||||
|
||||
SetStatusCodeAfterCacheExchangeMutator toTest = new SetStatusCodeAfterCacheExchangeMutator();
|
||||
toTest.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusCodeIs200_whenNoCacheHitsAndEvenNoCacheHeaderIsPresent() {
|
||||
CachedResponse cachedResponse = CachedResponse.create(HttpStatus.OK).build();
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("https://this")
|
||||
.header("Cache-Control", "no-cache").build();
|
||||
|
||||
inputExchange = MockServerWebExchange.from(httpRequest);
|
||||
|
||||
SetStatusCodeAfterCacheExchangeMutator toTest = new SetStatusCodeAfterCacheExchangeMutator();
|
||||
toTest.accept(inputExchange, cachedResponse);
|
||||
|
||||
assertThat(inputExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(inputExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.valueOf(statusCode));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user