HTTPCLIENT-2277: Aligned CachedResponseSuitabilityChecker with the specification requirements per RFC 9111 section 4
This commit is contained in:
parent
99eb13934f
commit
ebae9ef7e3
|
@ -342,6 +342,13 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
|
|||
return responseHeaders.headerIterator(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.3
|
||||
*/
|
||||
public MessageHeaders responseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Date value of the "Date" header or null if the header is missing or cannot be
|
||||
* parsed.
|
||||
|
@ -440,6 +447,13 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
|
|||
return requestURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.3
|
||||
*/
|
||||
public MessageHeaders requestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.3
|
||||
*/
|
||||
|
@ -447,6 +461,13 @@ public class HttpCacheEntry implements MessageHeaders, Serializable {
|
|||
return requestHeaders.headerIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 5.3
|
||||
*/
|
||||
public Iterator<Header> requestHeaderIterator(final String headerName) {
|
||||
return requestHeaders.headerIterator(headerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the given {@link HttpCacheEntry} is newer than the given {@link MessageHeaders}
|
||||
* by comparing values of their {@literal DATE} header. In case the given entry, or the message,
|
||||
|
|
|
@ -229,6 +229,9 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
}
|
||||
|
||||
final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Request cache control: {}", requestCacheControl);
|
||||
}
|
||||
|
||||
if (cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) {
|
||||
operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
|
||||
|
@ -242,6 +245,9 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
} else {
|
||||
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Response cache control: {}", responseCacheControl);
|
||||
}
|
||||
handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
}
|
||||
|
@ -590,21 +596,11 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
recordCacheHit(target, request);
|
||||
final Instant now = getCurrentDate();
|
||||
|
||||
if (requestCacheControl.isNoCache()) {
|
||||
// Revalidate with the server due to no-cache directive in response
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Revalidating with server due to no-cache directive in response.");
|
||||
}
|
||||
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
return;
|
||||
final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Request {} {}: {}", request.getMethod(), request.getRequestUri(), cacheSuitability);
|
||||
}
|
||||
|
||||
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, hit.entry, now)) {
|
||||
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, hit.entry)) {
|
||||
// Revalidate with the server due to no-cache directive in response
|
||||
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
return;
|
||||
}
|
||||
if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
|
||||
LOG.debug("Cache hit");
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
|
||||
|
@ -623,60 +619,71 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (!mayCallBackend(requestCacheControl)) {
|
||||
LOG.debug("Cache entry not suitable but only-if-cached requested");
|
||||
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(context);
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} else if (!(hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
|
||||
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
|
||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||
context.setExchangeId(exchangeId);
|
||||
final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
|
||||
exchangeId,
|
||||
scope.route,
|
||||
scope.originalRequest,
|
||||
new ComplexFuture<>(null),
|
||||
HttpClientContext.create(),
|
||||
scope.execRuntime.fork(),
|
||||
scope.scheduler,
|
||||
scope.execCount);
|
||||
cacheRevalidator.revalidateCacheEntry(
|
||||
hit.getEntryKey(),
|
||||
asyncExecCallback,
|
||||
asyncExecCallback1 -> revalidateCacheEntry(requestCacheControl, responseCacheControl,
|
||||
hit, target, request, entityProducer, fork, chain, asyncExecCallback1));
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex) {
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex2) {
|
||||
} else {
|
||||
if (!mayCallBackend(requestCacheControl)) {
|
||||
LOG.debug("Cache entry not is not fresh and only-if-cached requested");
|
||||
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(context);
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} else if (cacheSuitability == CacheSuitability.MISMATCH) {
|
||||
LOG.debug("Cache entry does not match the request; calling backend");
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
} else if (entityProducer != null && !entityProducer.isRepeatable()) {
|
||||
LOG.debug("Request is not repeatable; calling backend");
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
} else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
|
||||
LOG.debug("Cache entry with NOT_MODIFIED does not match the non-conditional request; calling backend");
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
} else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED || cacheSuitability == CacheSuitability.STALE) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
|
||||
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
|
||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||
context.setExchangeId(exchangeId);
|
||||
final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
|
||||
exchangeId,
|
||||
scope.route,
|
||||
scope.originalRequest,
|
||||
new ComplexFuture<>(null),
|
||||
HttpClientContext.create(),
|
||||
scope.execRuntime.fork(),
|
||||
scope.scheduler,
|
||||
scope.execCount);
|
||||
cacheRevalidator.revalidateCacheEntry(
|
||||
hit.getEntryKey(),
|
||||
asyncExecCallback,
|
||||
asyncExecCallback1 -> revalidateCacheEntry(requestCacheControl, responseCacheControl,
|
||||
hit, target, request, entityProducer, fork, chain, asyncExecCallback1));
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex) {
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Failed to generate cached response, falling back to backend", ex2);
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex2) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Failed to generate cached response, falling back to backend", ex2);
|
||||
}
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
} else {
|
||||
asyncExecCallback.failed(ex);
|
||||
}
|
||||
} else {
|
||||
asyncExecCallback.failed(ex);
|
||||
}
|
||||
} else {
|
||||
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
} else {
|
||||
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
LOG.debug("Cache entry not usable; calling backend");
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Cache entry not usable; calling backend");
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -773,8 +780,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
final Instant responseDate1 = getCurrentDate();
|
||||
|
||||
final AsyncExecCallback callback1;
|
||||
if (revalidationResponseIsTooOld(backendResponse1, hit.entry)
|
||||
&& (entityProducer == null || entityProducer.isRepeatable())) {
|
||||
if (revalidationResponseIsTooOld(backendResponse1, hit.entry)) {
|
||||
|
||||
final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
|
||||
BasicRequestBuilder.copy(scope.originalRequest).build());
|
||||
|
@ -880,14 +886,14 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
}
|
||||
|
||||
if (partialMatch != null && partialMatch.entry.hasVariants()) {
|
||||
if (partialMatch != null && partialMatch.entry.hasVariants() && entityProducer == null) {
|
||||
operation.setDependency(responseCache.getVariants(
|
||||
partialMatch,
|
||||
new FutureCallback<Collection<CacheHit>>() {
|
||||
|
||||
@Override
|
||||
public void completed(final Collection<CacheHit> variants) {
|
||||
if (variants != null && !variants.isEmpty() && (entityProducer == null || entityProducer.isRepeatable())) {
|
||||
if (variants != null && !variants.isEmpty()) {
|
||||
negotiateResponseFromVariants(target, request, entityProducer, scope, chain, asyncExecCallback, variants);
|
||||
} else {
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
|
|
|
@ -34,7 +34,11 @@ import java.util.Collection;
|
|||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.core5.annotation.Contract;
|
||||
|
@ -42,15 +46,22 @@ import org.apache.hc.core5.annotation.Internal;
|
|||
import org.apache.hc.core5.annotation.ThreadingBehavior;
|
||||
import org.apache.hc.core5.function.Resolver;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HeaderElement;
|
||||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.MessageHeaders;
|
||||
import org.apache.hc.core5.http.NameValuePair;
|
||||
import org.apache.hc.core5.http.URIScheme;
|
||||
import org.apache.hc.core5.http.message.BasicHeaderElementIterator;
|
||||
import org.apache.hc.core5.http.message.BasicHeaderValueFormatter;
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair;
|
||||
import org.apache.hc.core5.net.PercentCodec;
|
||||
import org.apache.hc.core5.net.URIAuthority;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.apache.hc.core5.util.Args;
|
||||
import org.apache.hc.core5.util.CharArrayBuffer;
|
||||
import org.apache.hc.core5.util.TextUtils;
|
||||
|
||||
/**
|
||||
* @since 4.1
|
||||
|
@ -201,6 +212,56 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
|
|||
return names;
|
||||
}
|
||||
|
||||
@Internal
|
||||
public static void normalizeElements(final MessageHeaders message, final String headerName, final Consumer<String> consumer) {
|
||||
// User-Agent as a special case due to its grammar
|
||||
if (headerName.equalsIgnoreCase(HttpHeaders.USER_AGENT)) {
|
||||
final Header header = message.getFirstHeader(headerName);
|
||||
if (header != null) {
|
||||
consumer.accept(header.getValue().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
} else {
|
||||
normalizeElements(message.headerIterator(headerName), consumer);
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
public static void normalizeElements(final Iterator<Header> iterator, final Consumer<String> consumer) {
|
||||
final Iterator<HeaderElement> it = new BasicHeaderElementIterator(iterator);
|
||||
StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.NONNULL), false)
|
||||
.filter(e -> !TextUtils.isBlank(e.getName()))
|
||||
.map(e -> {
|
||||
if (e.getValue() == null && e.getParameterCount() == 0) {
|
||||
return e.getName().toLowerCase(Locale.ROOT);
|
||||
} else {
|
||||
final CharArrayBuffer buf = new CharArrayBuffer(1024);
|
||||
BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
|
||||
buf,
|
||||
new BasicNameValuePair(
|
||||
e.getName().toLowerCase(Locale.ROOT),
|
||||
!TextUtils.isBlank(e.getValue()) ? e.getValue() : null),
|
||||
false);
|
||||
if (e.getParameterCount() > 0) {
|
||||
for (final NameValuePair nvp : e.getParameters()) {
|
||||
if (!TextUtils.isBlank(nvp.getName())) {
|
||||
buf.append(';');
|
||||
BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
|
||||
buf,
|
||||
new BasicNameValuePair(
|
||||
nvp.getName().toLowerCase(Locale.ROOT),
|
||||
!TextUtils.isBlank(nvp.getValue()) ? nvp.getValue() : null),
|
||||
false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
})
|
||||
.sorted()
|
||||
.distinct()
|
||||
.forEach(consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a "variant key" for the given request and the given variants.
|
||||
* @param request originating request
|
||||
|
@ -222,24 +283,13 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
|
|||
buf.append("&");
|
||||
}
|
||||
buf.append(PercentCodec.encode(h, StandardCharsets.UTF_8)).append("=");
|
||||
final List<String> tokens = new ArrayList<>();
|
||||
final Iterator<Header> headerIterator = request.headerIterator(h);
|
||||
while (headerIterator.hasNext()) {
|
||||
final Header header = headerIterator.next();
|
||||
CacheSupport.parseTokens(header, tokens::add);
|
||||
}
|
||||
final AtomicBoolean firstToken = new AtomicBoolean();
|
||||
tokens.stream()
|
||||
.filter(t -> !t.isEmpty())
|
||||
.map(t -> t.toLowerCase(Locale.ROOT))
|
||||
.sorted()
|
||||
.distinct()
|
||||
.forEach(t -> {
|
||||
if (!firstToken.compareAndSet(false, true)) {
|
||||
buf.append(",");
|
||||
}
|
||||
buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
|
||||
});
|
||||
normalizeElements(request, h, t -> {
|
||||
if (!firstToken.compareAndSet(false, true)) {
|
||||
buf.append(",");
|
||||
}
|
||||
buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
|
||||
});
|
||||
});
|
||||
buf.append("}");
|
||||
return buf.toString();
|
||||
|
|
44
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSuitability.java
vendored
Normal file
44
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSuitability.java
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* ====================================================================
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.
|
||||
* ====================================================================
|
||||
*
|
||||
* This software consists of voluntary contributions made by many
|
||||
* individuals on behalf of the Apache Software Foundation. For more
|
||||
* information on the Apache Software Foundation, please see
|
||||
* <http://www.apache.org/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
/**
|
||||
* @since 5.3
|
||||
*/
|
||||
enum CacheSuitability {
|
||||
|
||||
MISMATCH, // the cache entry does not match the request properties and cannot be used
|
||||
// to satisfy the request
|
||||
FRESH, // the cache entry is fresh and can be used to satisfy the request
|
||||
FRESH_ENOUGH, // the cache entry is deemed fresh enough and can be used to satisfy the request
|
||||
STALE, // the cache entry is stale and may be unsuitable to satisfy the request
|
||||
REVALIDATION_REQUIRED
|
||||
// the cache entry is stale and must not be used to satisfy the request
|
||||
// without revalidation
|
||||
|
||||
}
|
|
@ -41,6 +41,7 @@ class CacheValidityPolicy {
|
|||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CacheValidityPolicy.class);
|
||||
|
||||
private final boolean useHeuristicCaching;
|
||||
private final float heuristicCoefficient;
|
||||
private final TimeValue heuristicDefaultLifetime;
|
||||
|
||||
|
@ -53,6 +54,7 @@ class CacheValidityPolicy {
|
|||
*/
|
||||
CacheValidityPolicy(final CacheConfig config) {
|
||||
super();
|
||||
this.useHeuristicCaching = config != null ? config.isHeuristicCachingEnabled() : CacheConfig.DEFAULT.isHeuristicCachingEnabled();
|
||||
this.heuristicCoefficient = config != null ? config.getHeuristicCoefficient() : CacheConfig.DEFAULT.getHeuristicCoefficient();
|
||||
this.heuristicDefaultLifetime = config != null ? config.getHeuristicDefaultLifetime() : CacheConfig.DEFAULT.getHeuristicDefaultLifetime();
|
||||
}
|
||||
|
@ -117,35 +119,18 @@ class CacheValidityPolicy {
|
|||
}
|
||||
}
|
||||
|
||||
// No explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("No explicit expiration time present in the response. Using heuristic freshness lifetime calculation.");
|
||||
if (useHeuristicCaching) {
|
||||
// No explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("No explicit expiration time present in the response. Using heuristic freshness lifetime calculation.");
|
||||
}
|
||||
return getHeuristicFreshnessLifetime(entry);
|
||||
} else {
|
||||
return TimeValue.ZERO_MILLISECONDS;
|
||||
}
|
||||
return getHeuristicFreshnessLifetime(entry);
|
||||
}
|
||||
|
||||
public boolean isResponseFresh(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry,
|
||||
final Instant now) {
|
||||
return getCurrentAge(entry, now).compareTo(getFreshnessLifetime(responseCacheControl, entry)) == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides if this response is fresh enough based Last-Modified and Date, if available.
|
||||
* This entry is meant to be used when isResponseFresh returns false.
|
||||
*
|
||||
* The algorithm is as follows:
|
||||
* if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified),
|
||||
* else freshness lifetime is defaultLifetime
|
||||
*
|
||||
* @param entry the cache entry
|
||||
* @param now what time is it currently (When is right NOW)
|
||||
* @return {@code true} if the response is fresh
|
||||
*/
|
||||
public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry, final Instant now) {
|
||||
return getCurrentAge(entry, now).compareTo(getHeuristicFreshnessLifetime(entry)) == -1;
|
||||
}
|
||||
|
||||
public TimeValue getHeuristicFreshnessLifetime(final HttpCacheEntry entry) {
|
||||
TimeValue getHeuristicFreshnessLifetime(final HttpCacheEntry entry) {
|
||||
final Instant dateValue = entry.getInstant();
|
||||
final Instant lastModifiedValue = entry.getLastModified();
|
||||
|
||||
|
@ -189,7 +174,7 @@ class CacheValidityPolicy {
|
|||
staleness.compareTo(TimeValue.ofSeconds(responseCacheControl.getStaleIfError())) <= 0;
|
||||
}
|
||||
|
||||
protected TimeValue getApparentAge(final HttpCacheEntry entry) {
|
||||
TimeValue getApparentAge(final HttpCacheEntry entry) {
|
||||
final Instant dateValue = entry.getInstant();
|
||||
if (dateValue == null) {
|
||||
return CacheSupport.MAX_AGE;
|
||||
|
@ -216,7 +201,7 @@ class CacheValidityPolicy {
|
|||
* is negative. If the Age value is invalid (cannot be parsed into a number or contains non-numeric characters),
|
||||
* this method returns 0.
|
||||
*/
|
||||
protected long getAgeValue(final HttpCacheEntry entry) {
|
||||
long getAgeValue(final HttpCacheEntry entry) {
|
||||
final Header age = entry.getFirstHeader(HttpHeaders.AGE);
|
||||
if (age != null) {
|
||||
final AtomicReference<String> firstToken = new AtomicReference<>();
|
||||
|
@ -231,44 +216,29 @@ class CacheValidityPolicy {
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected TimeValue getCorrectedAgeValue(final HttpCacheEntry entry) {
|
||||
TimeValue getCorrectedAgeValue(final HttpCacheEntry entry) {
|
||||
final long ageValue = getAgeValue(entry);
|
||||
final long responseDelay = getResponseDelay(entry).toSeconds();
|
||||
return TimeValue.ofSeconds(ageValue + responseDelay);
|
||||
}
|
||||
|
||||
protected TimeValue getResponseDelay(final HttpCacheEntry entry) {
|
||||
TimeValue getResponseDelay(final HttpCacheEntry entry) {
|
||||
final Duration diff = Duration.between(entry.getRequestInstant(), entry.getResponseInstant());
|
||||
return TimeValue.ofSeconds(diff.getSeconds());
|
||||
}
|
||||
|
||||
protected TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
|
||||
TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
|
||||
final long apparentAge = getApparentAge(entry).toSeconds();
|
||||
final long correctedReceivedAge = getCorrectedAgeValue(entry).toSeconds();
|
||||
return TimeValue.ofSeconds(Math.max(apparentAge, correctedReceivedAge));
|
||||
}
|
||||
|
||||
protected TimeValue getResidentTime(final HttpCacheEntry entry, final Instant now) {
|
||||
TimeValue getResidentTime(final HttpCacheEntry entry, final Instant now) {
|
||||
final Duration diff = Duration.between(entry.getResponseInstant(), now);
|
||||
return TimeValue.ofSeconds(diff.getSeconds());
|
||||
}
|
||||
|
||||
|
||||
protected long getMaxAge(final ResponseCacheControl responseCacheControl) {
|
||||
final long maxAge = responseCacheControl.getMaxAge();
|
||||
final long sharedMaxAge = responseCacheControl.getSharedMaxAge();
|
||||
if (sharedMaxAge == -1) {
|
||||
return maxAge;
|
||||
} else if (maxAge == -1) {
|
||||
return sharedMaxAge;
|
||||
} else {
|
||||
return Math.min(maxAge, sharedMaxAge);
|
||||
}
|
||||
}
|
||||
|
||||
public TimeValue getStaleness(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry, final Instant now) {
|
||||
TimeValue getStaleness(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry, final Instant now) {
|
||||
final TimeValue age = getCurrentAge(entry, now);
|
||||
final TimeValue freshness = getFreshnessLifetime(responseCacheControl, entry);
|
||||
if (age.compareTo(freshness) <= 0) {
|
||||
|
@ -277,5 +247,4 @@ class CacheValidityPolicy {
|
|||
return TimeValue.ofSeconds(age.toSeconds() - freshness.toSeconds());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -26,8 +26,16 @@
|
|||
*/
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
|
@ -51,7 +59,6 @@ class CachedResponseSuitabilityChecker {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(CachedResponseSuitabilityChecker.class);
|
||||
|
||||
private final boolean sharedCache;
|
||||
private final boolean useHeuristicCaching;
|
||||
private final CacheValidityPolicy validityStrategy;
|
||||
|
||||
CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
|
||||
|
@ -59,140 +66,192 @@ class CachedResponseSuitabilityChecker {
|
|||
super();
|
||||
this.validityStrategy = validityStrategy;
|
||||
this.sharedCache = config.isSharedCache();
|
||||
this.useHeuristicCaching = config.isHeuristicCachingEnabled();
|
||||
}
|
||||
|
||||
CachedResponseSuitabilityChecker(final CacheConfig config) {
|
||||
this(new CacheValidityPolicy(config), config);
|
||||
}
|
||||
|
||||
private boolean isFreshEnough(final RequestCacheControl requestCacheControl,
|
||||
final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry,
|
||||
final Instant now) {
|
||||
if (validityStrategy.isResponseFresh(responseCacheControl, entry, now)) {
|
||||
return true;
|
||||
}
|
||||
if (useHeuristicCaching &&
|
||||
validityStrategy.isResponseHeuristicallyFresh(entry, now)) {
|
||||
return true;
|
||||
}
|
||||
if (originInsistsOnFreshness(responseCacheControl)) {
|
||||
return false;
|
||||
}
|
||||
if (requestCacheControl.getMaxStale() == -1) {
|
||||
return false;
|
||||
}
|
||||
return (requestCacheControl.getMaxStale() > validityStrategy.getStaleness(responseCacheControl, entry, now).toSeconds());
|
||||
}
|
||||
|
||||
private boolean originInsistsOnFreshness(final ResponseCacheControl responseCacheControl) {
|
||||
if (responseCacheControl.isMustRevalidate()) {
|
||||
return true;
|
||||
}
|
||||
if (!sharedCache) {
|
||||
return false;
|
||||
}
|
||||
return responseCacheControl.isProxyRevalidate() || responseCacheControl.getSharedMaxAge() >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
|
||||
* {@link HttpRequest}
|
||||
* Determine if I can utilize the given {@link HttpCacheEntry} to respond to the given
|
||||
* {@link HttpRequest}.
|
||||
*
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean canCachedResponseBeUsed(final RequestCacheControl requestCacheControl,
|
||||
final ResponseCacheControl responseCacheControl, final HttpRequest request,
|
||||
final HttpCacheEntry entry, final Instant now) {
|
||||
|
||||
if (isGetRequestWithHeadCacheEntry(request, entry)) {
|
||||
LOG.debug("Cache entry created by HEAD request cannot be used to serve GET request");
|
||||
return false;
|
||||
public CacheSuitability assessSuitability(final RequestCacheControl requestCacheControl,
|
||||
final ResponseCacheControl responseCacheControl,
|
||||
final HttpRequest request,
|
||||
final HttpCacheEntry entry,
|
||||
final Instant now) {
|
||||
if (!requestMethodMatch(request, entry)) {
|
||||
LOG.debug("Request method and the cache entry method do not match");
|
||||
return CacheSuitability.MISMATCH;
|
||||
}
|
||||
|
||||
if (!isFreshEnough(requestCacheControl, responseCacheControl, entry, now)) {
|
||||
LOG.debug("Cache entry is not fresh enough");
|
||||
return false;
|
||||
if (!requestUriMatch(request, entry)) {
|
||||
LOG.debug("Target request URI and the cache entry request URI do not match");
|
||||
return CacheSuitability.MISMATCH;
|
||||
}
|
||||
|
||||
if (!requestHeadersMatch(request, entry)) {
|
||||
LOG.debug("Request headers nominated by the cached response do not match those of the request associated with the cache entry");
|
||||
return CacheSuitability.MISMATCH;
|
||||
}
|
||||
|
||||
if (!requestHeadersMatch(request, entry)) {
|
||||
LOG.debug("Request headers nominated by the cached response do not match those of the request associated with the cache entry");
|
||||
return CacheSuitability.MISMATCH;
|
||||
}
|
||||
|
||||
if (requestCacheControl.isNoCache()) {
|
||||
LOG.debug("Request contained no-cache directive; the cache entry must be re-validated");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (isResponseNoCache(responseCacheControl, entry)) {
|
||||
LOG.debug("Response contained no-cache directive; the cache entry must be re-validated");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (hasUnsupportedConditionalHeaders(request)) {
|
||||
LOG.debug("Request contains unsupported conditional headers");
|
||||
return false;
|
||||
LOG.debug("Response from cache is not suitable due to the request containing unsupported conditional headers");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (!isConditional(request) && entry.getStatus() == HttpStatus.SC_NOT_MODIFIED) {
|
||||
LOG.debug("Unconditional request and non-modified cached response");
|
||||
return false;
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
|
||||
LOG.debug("Conditional request and with mismatched conditions");
|
||||
return false;
|
||||
if (!allConditionalsMatch(request, entry, now)) {
|
||||
LOG.debug("Response from cache is not suitable due to the conditional request and with mismatched conditions");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (hasUnsupportedCacheEntryForGet(request, entry)) {
|
||||
LOG.debug("HEAD response caching enabled but the cache entry does not contain a " +
|
||||
"request method, entity or a 204 response");
|
||||
return false;
|
||||
}
|
||||
if (requestCacheControl.isNoCache()) {
|
||||
LOG.debug("Response contained NO CACHE directive, cache was not suitable");
|
||||
return false;
|
||||
final TimeValue currentAge = validityStrategy.getCurrentAge(entry, now);
|
||||
final TimeValue freshnessLifetime = validityStrategy.getFreshnessLifetime(responseCacheControl, entry);
|
||||
|
||||
final boolean fresh = currentAge.compareTo(freshnessLifetime) < 0;
|
||||
|
||||
if (!fresh && responseCacheControl.isMustRevalidate()) {
|
||||
LOG.debug("Response from cache is not suitable due to the response must-revalidate requirement");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (requestCacheControl.isNoStore()) {
|
||||
LOG.debug("Response contained NO STORE directive, cache was not suitable");
|
||||
return false;
|
||||
if (!fresh && sharedCache && responseCacheControl.isProxyRevalidate()) {
|
||||
LOG.debug("Response from cache is not suitable due to the response proxy-revalidate requirement");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
|
||||
if (requestCacheControl.getMaxAge() >= 0) {
|
||||
if (validityStrategy.getCurrentAge(entry, now).toSeconds() > requestCacheControl.getMaxAge()) {
|
||||
LOG.debug("Response from cache was not suitable due to max age");
|
||||
return false;
|
||||
if (fresh && requestCacheControl.getMaxAge() >= 0) {
|
||||
if (currentAge.toSeconds() > requestCacheControl.getMaxAge() && requestCacheControl.getMaxStale() == -1) {
|
||||
LOG.debug("Response from cache is not suitable due to the request max-age requirement");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
}
|
||||
|
||||
if (fresh && requestCacheControl.getMinFresh() >= 0) {
|
||||
if (requestCacheControl.getMinFresh() == 0 ||
|
||||
freshnessLifetime.toSeconds() - currentAge.toSeconds() < requestCacheControl.getMinFresh()) {
|
||||
LOG.debug("Response from cache is not suitable due to the request min-fresh requirement");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCacheControl.getMaxStale() >= 0) {
|
||||
if (validityStrategy.getFreshnessLifetime(responseCacheControl, entry).toSeconds() > requestCacheControl.getMaxStale()) {
|
||||
LOG.debug("Response from cache was not suitable due to max stale freshness");
|
||||
return false;
|
||||
final long stale = currentAge.compareTo(freshnessLifetime) > 0 ? currentAge.toSeconds() - freshnessLifetime.toSeconds() : 0;
|
||||
if (stale >= requestCacheControl.getMaxStale()) {
|
||||
LOG.debug("Response from cache is not suitable due to the request max-stale requirement");
|
||||
return CacheSuitability.REVALIDATION_REQUIRED;
|
||||
} else {
|
||||
LOG.debug("The cache entry is fresh enough");
|
||||
return CacheSuitability.FRESH_ENOUGH;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCacheControl.getMinFresh() >= 0) {
|
||||
if (requestCacheControl.getMinFresh() == 0) {
|
||||
return false;
|
||||
if (fresh) {
|
||||
LOG.debug("The cache entry is fresh");
|
||||
return CacheSuitability.FRESH;
|
||||
} else {
|
||||
LOG.debug("The cache entry is stale");
|
||||
return CacheSuitability.STALE;
|
||||
}
|
||||
}
|
||||
|
||||
boolean requestMethodMatch(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
return request.getMethod().equalsIgnoreCase(entry.getRequestMethod()) ||
|
||||
(Method.HEAD.isSame(request.getMethod()) && Method.GET.isSame(entry.getRequestMethod()));
|
||||
}
|
||||
|
||||
boolean requestUriMatch(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
try {
|
||||
final URI requestURI = CacheKeyGenerator.normalize(request.getUri());
|
||||
final URI cacheURI = new URI(entry.getRequestURI());
|
||||
if (requestURI.isAbsolute()) {
|
||||
return Objects.equals(requestURI, cacheURI);
|
||||
} else {
|
||||
return Objects.equals(requestURI.getPath(), cacheURI.getPath()) && Objects.equals(requestURI.getQuery(), cacheURI.getQuery());
|
||||
}
|
||||
final TimeValue age = validityStrategy.getCurrentAge(entry, now);
|
||||
final TimeValue freshness = validityStrategy.getFreshnessLifetime(responseCacheControl, entry);
|
||||
if (freshness.toSeconds() - age.toSeconds() < requestCacheControl.getMinFresh()) {
|
||||
LOG.debug("Response from cache was not suitable due to min fresh " +
|
||||
"freshness requirement");
|
||||
return false;
|
||||
} catch (final URISyntaxException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean requestHeadersMatch(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
final Iterator<Header> it = entry.headerIterator(HttpHeaders.VARY);
|
||||
if (it.hasNext()) {
|
||||
final Set<String> headerNames = new HashSet<>();
|
||||
while (it.hasNext()) {
|
||||
final Header header = it.next();
|
||||
CacheSupport.parseTokens(header, e -> {
|
||||
headerNames.add(e.toLowerCase(Locale.ROOT));
|
||||
});
|
||||
}
|
||||
final List<String> tokensInRequest = new ArrayList<>();
|
||||
final List<String> tokensInCache = new ArrayList<>();
|
||||
for (final String headerName: headerNames) {
|
||||
if (headerName.equalsIgnoreCase("*")) {
|
||||
return false;
|
||||
}
|
||||
CacheKeyGenerator.normalizeElements(request, headerName, tokensInRequest::add);
|
||||
CacheKeyGenerator.normalizeElements(entry.requestHeaders(), headerName, tokensInCache::add);
|
||||
if (!Objects.equals(tokensInRequest, tokensInCache)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug("Response from cache was suitable");
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isGet(final HttpRequest request) {
|
||||
return Method.GET.isSame(request.getMethod());
|
||||
}
|
||||
|
||||
private boolean isHead(final HttpRequest request) {
|
||||
return Method.HEAD.isSame(request.getMethod());
|
||||
}
|
||||
|
||||
private boolean entryIsNotA204Response(final HttpCacheEntry entry) {
|
||||
return entry.getStatus() != HttpStatus.SC_NO_CONTENT;
|
||||
}
|
||||
|
||||
private boolean cacheEntryDoesNotContainMethodAndEntity(final HttpCacheEntry entry) {
|
||||
return entry.getRequestMethod() == null && entry.getResource() == null;
|
||||
}
|
||||
|
||||
private boolean hasUnsupportedCacheEntryForGet(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
return isGet(request) && cacheEntryDoesNotContainMethodAndEntity(entry) && entryIsNotA204Response(entry);
|
||||
/**
|
||||
* Determines if the given {@link HttpCacheEntry} requires revalidation based on the presence of the {@code no-cache} directive
|
||||
* in the Cache-Control header.
|
||||
* <p>
|
||||
* The method returns true in the following cases:
|
||||
* - If the {@code no-cache} directive is present without any field names (unqualified).
|
||||
* - If the {@code no-cache} directive is present with field names, and at least one of these field names is present
|
||||
* in the headers of the {@link HttpCacheEntry}.
|
||||
* <p>
|
||||
* If the {@code no-cache} directive is not present in the Cache-Control header, the method returns {@code false}.
|
||||
*/
|
||||
boolean isResponseNoCache(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry) {
|
||||
// If no-cache directive is present and has no field names
|
||||
if (responseCacheControl.isNoCache()) {
|
||||
final Set<String> noCacheFields = responseCacheControl.getNoCacheFields();
|
||||
if (noCacheFields.isEmpty()) {
|
||||
LOG.debug("Revalidation required due to unqualified no-cache directive");
|
||||
return true;
|
||||
}
|
||||
for (final String field : noCacheFields) {
|
||||
if (entry.containsHeader(field)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Revalidation required due to no-cache directive with field {}", field);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,22 +263,6 @@ class CachedResponseSuitabilityChecker {
|
|||
return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given request is a {@link org.apache.hc.core5.http.Method#GET} request and the
|
||||
* associated cache entry was created by a {@link org.apache.hc.core5.http.Method#HEAD} request.
|
||||
*
|
||||
* @param request The {@link HttpRequest} to check if it is a {@link org.apache.hc.core5.http.Method#GET} request.
|
||||
* @param entry The {@link HttpCacheEntry} to check if it was created by
|
||||
* a {@link org.apache.hc.core5.http.Method#HEAD} request.
|
||||
* @return true if the request is a {@link org.apache.hc.core5.http.Method#GET} request and the cache entry was
|
||||
* created by a {@link org.apache.hc.core5.http.Method#HEAD} request, otherwise {@code false}.
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean isGetRequestWithHeadCacheEntry(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
return isGet(request) && Method.HEAD.isSame(entry.getRequestMethod());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that conditionals that are part of this request match
|
||||
* @param request The current httpRequest being made
|
||||
|
@ -231,6 +274,10 @@ class CachedResponseSuitabilityChecker {
|
|||
final boolean hasEtagValidator = hasSupportedEtagValidator(request);
|
||||
final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
|
||||
|
||||
if (!hasEtagValidator && !hasLastModifiedValidator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
|
||||
final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
|
||||
|
||||
|
@ -244,18 +291,18 @@ class CachedResponseSuitabilityChecker {
|
|||
return !hasLastModifiedValidator || lastModifiedValidatorMatches;
|
||||
}
|
||||
|
||||
private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
|
||||
return (request.getFirstHeader(HttpHeaders.IF_RANGE) != null
|
||||
|| request.getFirstHeader(HttpHeaders.IF_MATCH) != null
|
||||
|| hasValidDateField(request, HttpHeaders.IF_UNMODIFIED_SINCE));
|
||||
boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
|
||||
return (request.containsHeader(HttpHeaders.IF_RANGE)
|
||||
|| request.containsHeader(HttpHeaders.IF_MATCH)
|
||||
|| request.containsHeader(HttpHeaders.IF_UNMODIFIED_SINCE));
|
||||
}
|
||||
|
||||
private boolean hasSupportedEtagValidator(final HttpRequest request) {
|
||||
boolean hasSupportedEtagValidator(final HttpRequest request) {
|
||||
return request.containsHeader(HttpHeaders.IF_NONE_MATCH);
|
||||
}
|
||||
|
||||
private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
|
||||
return hasValidDateField(request, HttpHeaders.IF_MODIFIED_SINCE);
|
||||
boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
|
||||
return request.containsHeader(HttpHeaders.IF_MODIFIED_SINCE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -264,7 +311,7 @@ class CachedResponseSuitabilityChecker {
|
|||
* @param entry the cache entry
|
||||
* @return boolean does the etag validator match
|
||||
*/
|
||||
private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
|
||||
final Header etagHeader = entry.getFirstHeader(HttpHeaders.ETAG);
|
||||
final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
|
||||
final Iterator<HeaderElement> it = MessageSupport.iterate(request, HttpHeaders.IF_NONE_MATCH);
|
||||
|
@ -285,7 +332,7 @@ class CachedResponseSuitabilityChecker {
|
|||
* @param now right NOW in time
|
||||
* @return boolean Does the last modified header match
|
||||
*/
|
||||
private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
|
||||
boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
|
||||
final Instant lastModified = entry.getLastModified();
|
||||
if (lastModified == null) {
|
||||
return false;
|
||||
|
@ -302,11 +349,4 @@ class CachedResponseSuitabilityChecker {
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean hasValidDateField(final HttpRequest request, final String headerName) {
|
||||
for(final Header h : request.getHeaders(headerName)) {
|
||||
final Instant instant = DateUtils.parseStandardDate(h.getValue());
|
||||
return instant != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -168,6 +168,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
final CacheHit root = result != null ? result.root : null;
|
||||
|
||||
final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Request cache control: {}", requestCacheControl);
|
||||
}
|
||||
if (!cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) {
|
||||
LOG.debug("Request is not servable from cache");
|
||||
return callBackend(target, request, scope, chain);
|
||||
|
@ -179,6 +182,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
return handleCacheMiss(requestCacheControl, root, target, request, scope, chain);
|
||||
} else {
|
||||
final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Response cache control: {}", responseCacheControl);
|
||||
}
|
||||
return handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
|
||||
}
|
||||
}
|
||||
|
@ -238,19 +244,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
recordCacheHit(target, request);
|
||||
final Instant now = getCurrentDate();
|
||||
|
||||
if (requestCacheControl.isNoCache()) {
|
||||
// Revalidate with the server
|
||||
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
|
||||
final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Request {} {}: {}", request.getMethod(), request.getRequestUri(), cacheSuitability);
|
||||
}
|
||||
|
||||
if (suitabilityChecker.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, hit.entry, now)) {
|
||||
if (responseCachingPolicy.responseContainsNoCacheDirective(responseCacheControl, hit.entry)) {
|
||||
// Revalidate with the server due to no-cache directive in response
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Revalidating with server due to no-cache directive in response.");
|
||||
}
|
||||
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
|
||||
}
|
||||
if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
|
||||
LOG.debug("Cache hit");
|
||||
try {
|
||||
return convert(generateCachedResponse(hit.entry, request, context), scope);
|
||||
|
@ -262,44 +260,55 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
setResponseStatus(scope.clientContext, CacheResponseStatus.FAILURE);
|
||||
return chain.proceed(request, scope);
|
||||
}
|
||||
} else if (!mayCallBackend(requestCacheControl)) {
|
||||
LOG.debug("Cache entry not suitable but only-if-cached requested");
|
||||
return convert(generateGatewayTimeout(context), scope);
|
||||
} else if (!(hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
|
||||
try {
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
|
||||
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||
context.setExchangeId(exchangeId);
|
||||
final ExecChain.Scope fork = new ExecChain.Scope(
|
||||
exchangeId,
|
||||
scope.route,
|
||||
scope.originalRequest,
|
||||
scope.execRuntime.fork(null),
|
||||
HttpClientContext.create());
|
||||
final SimpleHttpResponse response = generateCachedResponse(hit.entry, request, context);
|
||||
cacheRevalidator.revalidateCacheEntry(
|
||||
hit.getEntryKey(),
|
||||
() -> revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, fork, chain));
|
||||
return convert(response, scope);
|
||||
}
|
||||
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
|
||||
} catch (final IOException ioex) {
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
return convert(generateCachedResponse(hit.entry, request, context), scope);
|
||||
}
|
||||
return convert(handleRevalidationFailure(requestCacheControl, responseCacheControl, hit.entry, request, context, now), scope);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Cache entry not usable; calling backend");
|
||||
return callBackend(target, request, scope, chain);
|
||||
if (!mayCallBackend(requestCacheControl)) {
|
||||
LOG.debug("Cache entry not is not fresh and only-if-cached requested");
|
||||
return convert(generateGatewayTimeout(context), scope);
|
||||
} else if (cacheSuitability == CacheSuitability.MISMATCH) {
|
||||
LOG.debug("Cache entry does not match the request; calling backend");
|
||||
return callBackend(target, request, scope, chain);
|
||||
} else if (request.getEntity() != null && !request.getEntity().isRepeatable()) {
|
||||
LOG.debug("Request is not repeatable; calling backend");
|
||||
return callBackend(target, request, scope, chain);
|
||||
} else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
|
||||
LOG.debug("Cache entry with NOT_MODIFIED does not match the non-conditional request; calling backend");
|
||||
return callBackend(target, request, scope, chain);
|
||||
} else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED || cacheSuitability == CacheSuitability.STALE) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(responseCacheControl, hit.entry);
|
||||
try {
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, now)
|
||||
&& (validityPolicy.mayReturnStaleWhileRevalidating(responseCacheControl, hit.entry, now) || staleIfErrorEnabled)) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||
context.setExchangeId(exchangeId);
|
||||
final ExecChain.Scope fork = new ExecChain.Scope(
|
||||
exchangeId,
|
||||
scope.route,
|
||||
scope.originalRequest,
|
||||
scope.execRuntime.fork(null),
|
||||
HttpClientContext.create());
|
||||
final SimpleHttpResponse response = generateCachedResponse(hit.entry, request, context);
|
||||
cacheRevalidator.revalidateCacheEntry(
|
||||
hit.getEntryKey(),
|
||||
() -> revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, fork, chain));
|
||||
return convert(response, scope);
|
||||
}
|
||||
return revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
|
||||
} catch (final IOException ioex) {
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
return convert(generateCachedResponse(hit.entry, request, context), scope);
|
||||
}
|
||||
return convert(handleRevalidationFailure(requestCacheControl, responseCacheControl, hit.entry, request, context, now), scope);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Cache entry not usable; calling backend");
|
||||
return callBackend(target, request, scope, chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,7 +467,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
if (!mayCallBackend(requestCacheControl)) {
|
||||
return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
|
||||
}
|
||||
if (partialMatch != null && partialMatch.entry.hasVariants()) {
|
||||
if (partialMatch != null && partialMatch.entry.hasVariants() && request.getEntity() == null) {
|
||||
final List<CacheHit> variants = responseCache.getVariants(partialMatch);
|
||||
if (variants != null && !variants.isEmpty()) {
|
||||
return negotiateResponseFromVariants(target, request, scope, chain, variants);
|
||||
|
@ -509,8 +518,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
return callBackend(target, request, scope, chain);
|
||||
}
|
||||
|
||||
if (revalidationResponseIsTooOld(backendResponse, match.entry)
|
||||
&& (request.getEntity() == null || request.getEntity().isRepeatable())) {
|
||||
if (revalidationResponseIsTooOld(backendResponse, match.entry)) {
|
||||
final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
|
||||
return callBackend(target, unconditional, scope, chain);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@
|
|||
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hc.core5.annotation.Contract;
|
||||
|
@ -226,7 +228,6 @@ final class ResponseCacheControl implements CacheControl {
|
|||
return mustRevalidate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the proxy-revalidate value is set in the Cache-Control header.
|
||||
*
|
||||
|
@ -433,6 +434,12 @@ final class ResponseCacheControl implements CacheControl {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setNoCacheFields(final String... noCacheFields) {
|
||||
this.noCacheFields = new HashSet<>();
|
||||
this.noCacheFields.addAll(Arrays.asList(noCacheFields));
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isMustUnderstand() {
|
||||
return mustUnderstand;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ package org.apache.hc.client5.http.impl.cache;
|
|||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
|
@ -397,43 +396,6 @@ class ResponseCachingPolicy {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given {@link HttpCacheEntry} requires revalidation based on the presence of the {@code no-cache} directive
|
||||
* in the Cache-Control header.
|
||||
* <p>
|
||||
* The method returns true in the following cases:
|
||||
* - If the {@code no-cache} directive is present without any field names.
|
||||
* - If the {@code no-cache} directive is present with field names, and at least one of these field names is present
|
||||
* in the headers of the {@link HttpCacheEntry}.
|
||||
* <p>
|
||||
* If the {@code no-cache} directive is not present in the Cache-Control header, the method returns {@code false}.
|
||||
*
|
||||
* @param entry the {@link HttpCacheEntry} containing the headers to check for the {@code no-cache} directive.
|
||||
* @return true if revalidation is required based on the {@code no-cache} directive, {@code false} otherwise.
|
||||
*/
|
||||
boolean responseContainsNoCacheDirective(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry) {
|
||||
final Set<String> noCacheFields = responseCacheControl.getNoCacheFields();
|
||||
|
||||
// If no-cache directive is present and has no field names
|
||||
if (responseCacheControl.isNoCache() && noCacheFields.isEmpty()) {
|
||||
LOG.debug("No-cache directive present without field names. Revalidation required.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no-cache directive is present with field names
|
||||
if (responseCacheControl.isNoCache()) {
|
||||
for (final String field : noCacheFields) {
|
||||
if (entry.getFirstHeader(field) != null) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("No-cache directive field '{}' found in response headers. Revalidation required.", field);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Understood status codes include:
|
||||
* - All 2xx (Successful) status codes (200-299)
|
||||
|
|
|
@ -28,15 +28,20 @@ package org.apache.hc.client5.http.impl.cache;
|
|||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.message.BasicHeader;
|
||||
import org.apache.hc.core5.http.message.BasicHeaderIterator;
|
||||
import org.apache.hc.core5.http.message.BasicHttpRequest;
|
||||
import org.apache.hc.core5.http.support.BasicRequestBuilder;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
@ -295,6 +300,41 @@ public class TestCacheKeyGenerator {
|
|||
"/full_episodes?foo=bar")));
|
||||
}
|
||||
|
||||
private static Iterator<Header> headers(final Header... headers) {
|
||||
return new BasicHeaderIterator(headers, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeHeaderElements() {
|
||||
final List<String> tokens = new ArrayList<>();
|
||||
CacheKeyGenerator.normalizeElements(headers(
|
||||
new BasicHeader("Accept-Encoding", "gzip,zip,deflate")
|
||||
), tokens::add);
|
||||
Assertions.assertEquals(Arrays.asList("deflate", "gzip", "zip"), tokens);
|
||||
|
||||
tokens.clear();
|
||||
CacheKeyGenerator.normalizeElements(headers(
|
||||
new BasicHeader("Accept-Encoding", " gZip , Zip, , , deflate ")
|
||||
), tokens::add);
|
||||
Assertions.assertEquals(Arrays.asList("deflate", "gzip", "zip"), tokens);
|
||||
|
||||
tokens.clear();
|
||||
CacheKeyGenerator.normalizeElements(headers(
|
||||
new BasicHeader("Accept-Encoding", "gZip,Zip,,"),
|
||||
new BasicHeader("Accept-Encoding", " gZip,Zip,,,"),
|
||||
new BasicHeader("Accept-Encoding", "gZip, ,,,deflate")
|
||||
), tokens::add);
|
||||
Assertions.assertEquals(Arrays.asList("deflate", "gzip", "zip"), tokens);
|
||||
|
||||
tokens.clear();
|
||||
CacheKeyGenerator.normalizeElements(headers(
|
||||
new BasicHeader("Cookie", "name1 = value1 ; p1 = v1 ; P2 = \"v2\""),
|
||||
new BasicHeader("Cookie", "name3;;;"),
|
||||
new BasicHeader("Cookie", " name2 = \" value 2 \" ; ; ; ,,,")
|
||||
), tokens::add);
|
||||
Assertions.assertEquals(Arrays.asList("name1=value1;p1=v1;p2=v2", "name2=\" value 2 \"", "name3"), tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetVariantKey() {
|
||||
final HttpRequest request = BasicRequestBuilder.get("/blah")
|
||||
|
|
|
@ -29,7 +29,6 @@ package org.apache.hc.client5.http.impl.cache;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
|
@ -244,26 +243,6 @@ public class TestCacheValidityPolicy {
|
|||
assertTrue(TimeValue.isNonNegative(impl.getHeuristicFreshnessLifetime(entry)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseIsFreshIfFreshnessLifetimeExceedsCurrentAge() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
|
||||
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder().build();
|
||||
impl = new CacheValidityPolicy() {
|
||||
@Override
|
||||
public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
|
||||
assertSame(entry, e);
|
||||
assertEquals(now, d);
|
||||
return TimeValue.ofSeconds(6);
|
||||
}
|
||||
@Override
|
||||
public TimeValue getFreshnessLifetime(final ResponseCacheControl cacheControl, final HttpCacheEntry e) {
|
||||
assertSame(entry, e);
|
||||
return TimeValue.ofSeconds(10);
|
||||
}
|
||||
};
|
||||
assertTrue(impl.isResponseFresh(responseCacheControl, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeuristicFreshnessLifetimeCustomProperly() {
|
||||
final CacheConfig cacheConfig = CacheConfig.custom().setHeuristicDefaultLifetime(TimeValue.ofSeconds(10))
|
||||
|
@ -274,46 +253,6 @@ public class TestCacheValidityPolicy {
|
|||
assertEquals(defaultFreshness, impl.getHeuristicFreshnessLifetime(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseIsNotFreshIfFreshnessLifetimeEqualsCurrentAge() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
|
||||
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder().build();
|
||||
impl = new CacheValidityPolicy() {
|
||||
@Override
|
||||
public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
|
||||
assertEquals(now, d);
|
||||
assertSame(entry, e);
|
||||
return TimeValue.ofSeconds(6);
|
||||
}
|
||||
@Override
|
||||
public TimeValue getFreshnessLifetime(final ResponseCacheControl cacheControl, final HttpCacheEntry e) {
|
||||
assertSame(entry, e);
|
||||
return TimeValue.ofSeconds(6);
|
||||
}
|
||||
};
|
||||
assertFalse(impl.isResponseFresh(responseCacheControl, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseIsNotFreshIfCurrentAgeExceedsFreshnessLifetime() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
|
||||
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder().build();
|
||||
impl = new CacheValidityPolicy() {
|
||||
@Override
|
||||
public TimeValue getCurrentAge(final HttpCacheEntry e, final Instant d) {
|
||||
assertEquals(now, d);
|
||||
assertSame(entry, e);
|
||||
return TimeValue.ofSeconds(10);
|
||||
}
|
||||
@Override
|
||||
public TimeValue getFreshnessLifetime(final ResponseCacheControl cacheControl, final HttpCacheEntry e) {
|
||||
assertSame(entry, e);
|
||||
return TimeValue.ofSeconds(6);
|
||||
}
|
||||
};
|
||||
assertFalse(impl.isResponseFresh(responseCacheControl, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheEntryIsRevalidatableIfHeadersIncludeETag() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
|
|
|
@ -31,11 +31,12 @@ import java.time.Instant;
|
|||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.apache.hc.core5.http.Method;
|
||||
import org.apache.hc.core5.http.message.BasicHeader;
|
||||
import org.apache.hc.core5.http.message.BasicHttpRequest;
|
||||
import org.apache.hc.core5.http.support.BasicRequestBuilder;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -69,32 +70,204 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
impl = new CachedResponseSuitabilityChecker(CacheConfig.DEFAULT);
|
||||
}
|
||||
|
||||
private HttpCacheEntry getEntry(final Header[] headers) {
|
||||
return HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo, headers);
|
||||
private HttpCacheEntry makeEntry(final Instant requestDate,
|
||||
final Instant responseDate,
|
||||
final Method method,
|
||||
final String requestUri,
|
||||
final Header[] requestHeaders,
|
||||
final int status,
|
||||
final Header[] responseHeaders) {
|
||||
return HttpTestUtils.makeCacheEntry(requestDate, responseDate, method, requestUri, requestHeaders,
|
||||
status, responseHeaders, HttpTestUtils.makeNullResource());
|
||||
}
|
||||
|
||||
private HttpCacheEntry makeEntry(final Header... headers) {
|
||||
return makeEntry(elevenSecondsAgo, nineSecondsAgo, Method.GET, "/foo", null, 200, headers);
|
||||
}
|
||||
|
||||
private HttpCacheEntry makeEntry(final Instant requestDate,
|
||||
final Instant responseDate,
|
||||
final Header... headers) {
|
||||
return makeEntry(requestDate, responseDate, Method.GET, "/foo", null, 200, headers);
|
||||
}
|
||||
|
||||
private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header... headers) {
|
||||
return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, null, 200, headers);
|
||||
}
|
||||
|
||||
private HttpCacheEntry makeEntry(final Method method, final String requestUri, final Header[] requestHeaders,
|
||||
final int status, final Header[] responseHeaders) {
|
||||
return makeEntry(elevenSecondsAgo, nineSecondsAgo, method, requestUri, requestHeaders,
|
||||
status, responseHeaders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestMethodMatch() {
|
||||
request = new BasicHttpRequest("GET", "/foo");
|
||||
entry = makeEntry(Method.GET, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("HEAD", "/foo");
|
||||
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("POST", "/foo");
|
||||
Assertions.assertFalse(impl.requestMethodMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("HEAD", "/foo");
|
||||
entry = makeEntry(Method.HEAD, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertTrue(impl.requestMethodMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("GET", "/foo");
|
||||
entry = makeEntry(Method.HEAD, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertFalse(impl.requestMethodMatch(request, entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestUriMatch() {
|
||||
request = new BasicHttpRequest("GET", "/foo");
|
||||
entry = makeEntry(Method.GET, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertTrue(impl.requestUriMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo");
|
||||
entry = makeEntry(Method.GET, "http://some-host:80/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertTrue(impl.requestUriMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("GET", new HttpHost("Some-Host"), "/foo?bar");
|
||||
entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertTrue(impl.requestUriMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("GET", new HttpHost("some-other-host"), "/foo");
|
||||
entry = makeEntry(Method.GET, "http://some-host:80/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertFalse(impl.requestUriMatch(request, entry));
|
||||
|
||||
request = new BasicHttpRequest("GET", new HttpHost("some-host"), "/foo?huh");
|
||||
entry = makeEntry(Method.GET, "http://some-host:80/foo?bar",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertFalse(impl.requestUriMatch(request, entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestHeadersMatch() {
|
||||
request = BasicRequestBuilder.get("/foo").build();
|
||||
entry = makeEntry(
|
||||
Method.GET, "/foo",
|
||||
new Header[]{},
|
||||
200,
|
||||
new Header[]{
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
});
|
||||
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
|
||||
|
||||
request = BasicRequestBuilder.get("/foo").build();
|
||||
entry = makeEntry(
|
||||
Method.GET, "/foo",
|
||||
new Header[]{},
|
||||
200,
|
||||
new Header[]{
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
new BasicHeader("Vary", "")
|
||||
});
|
||||
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
|
||||
|
||||
request = BasicRequestBuilder.get("/foo")
|
||||
.addHeader("Accept-Encoding", "blah")
|
||||
.build();
|
||||
entry = makeEntry(
|
||||
Method.GET, "/foo",
|
||||
new Header[]{
|
||||
new BasicHeader("Accept-Encoding", "blah")
|
||||
},
|
||||
200,
|
||||
new Header[]{
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
new BasicHeader("Vary", "Accept-Encoding")
|
||||
});
|
||||
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
|
||||
|
||||
request = BasicRequestBuilder.get("/foo")
|
||||
.addHeader("Accept-Encoding", "gzip, deflate, deflate , zip, ")
|
||||
.build();
|
||||
entry = makeEntry(
|
||||
Method.GET, "/foo",
|
||||
new Header[]{
|
||||
new BasicHeader("Accept-Encoding", " gzip, zip, deflate")
|
||||
},
|
||||
200,
|
||||
new Header[]{
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
new BasicHeader("Vary", "Accept-Encoding")
|
||||
});
|
||||
Assertions.assertTrue(impl.requestHeadersMatch(request, entry));
|
||||
|
||||
request = BasicRequestBuilder.get("/foo")
|
||||
.addHeader("Accept-Encoding", "gzip, deflate, zip")
|
||||
.build();
|
||||
entry = makeEntry(
|
||||
Method.GET, "/foo",
|
||||
new Header[]{
|
||||
new BasicHeader("Accept-Encoding", " gzip, deflate")
|
||||
},
|
||||
200,
|
||||
new Header[]{
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
new BasicHeader("Vary", "Accept-Encoding")
|
||||
});
|
||||
Assertions.assertFalse(impl.requestHeadersMatch(request, entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResponseNoCache() {
|
||||
entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setNoCache(false)
|
||||
.build();
|
||||
|
||||
Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
|
||||
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setNoCache(true)
|
||||
.build();
|
||||
|
||||
Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
|
||||
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setNoCache(true)
|
||||
.setNoCacheFields("stuff", "more-stuff")
|
||||
.build();
|
||||
|
||||
Assertions.assertFalse(impl.isResponseNoCache(responseCacheControl, entry));
|
||||
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
new BasicHeader("stuff", "booh"));
|
||||
|
||||
Assertions.assertTrue(impl.isResponseNoCache(responseCacheControl, entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuitableIfCacheEntryIsFresh() {
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotSuitableIfCacheEntryIsNotFresh() {
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(5)
|
||||
.build();
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.STALE, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -102,14 +275,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setNoCache(true)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -117,14 +288,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setMaxAge(10)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
entry = getEntry(headers);
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -132,14 +301,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setMaxAge(15)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -147,14 +314,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setMinFresh(10)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -162,14 +327,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setMinFresh(10)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(15)
|
||||
.build();
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -180,11 +343,11 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(headers);
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(5)
|
||||
.build();
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH_ENOUGH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -192,14 +355,12 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
requestCacheControl = RequestCacheControl.builder()
|
||||
.setMaxStale(2)
|
||||
.build();
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(5)
|
||||
.build();
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.REVALIDATION_REQUIRED, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -207,28 +368,22 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
final Instant oneSecondAgo = now.minusSeconds(1);
|
||||
final Instant twentyOneSecondsAgo = now.minusSeconds(21);
|
||||
|
||||
final Header[] headers = {
|
||||
entry = makeEntry(oneSecondAgo, oneSecondAgo,
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
|
||||
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(twentyOneSecondsAgo))
|
||||
};
|
||||
|
||||
entry = HttpTestUtils.makeCacheEntry(oneSecondAgo, oneSecondAgo, headers);
|
||||
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(twentyOneSecondsAgo)));
|
||||
|
||||
final CacheConfig config = CacheConfig.custom()
|
||||
.setHeuristicCachingEnabled(true)
|
||||
.setHeuristicCoefficient(0.1f).build();
|
||||
impl = new CachedResponseSuitabilityChecker(config);
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuitableIfCacheEntryIsHeuristicallyFreshEnoughByDefault() {
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
|
||||
final CacheConfig config = CacheConfig.custom()
|
||||
.setHeuristicCachingEnabled(true)
|
||||
|
@ -236,71 +391,43 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
.build();
|
||||
impl = new CachedResponseSuitabilityChecker(config);
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuitableIfRequestMethodisHEAD() {
|
||||
final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = getEntry(headers);
|
||||
entry = makeEntry(
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, headRequest, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotSuitableIfRequestMethodIsGETAndEntryResourceIsNull() {
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo,
|
||||
Method.HEAD, "/", null,
|
||||
HttpStatus.SC_OK, headers,
|
||||
HttpTestUtils.makeNullResource());
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuitableForGETIfEntryDoesNotSpecifyARequestMethodButContainsEntity() {
|
||||
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
};
|
||||
entry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo,
|
||||
Method.GET, "/", null,
|
||||
HttpStatus.SC_OK, headers,
|
||||
HttpTestUtils.makeRandomResource(128));
|
||||
entry = makeEntry(Method.GET, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuitableForGETIfHeadResponseCachingEnabledAndEntryDoesNotSpecifyARequestMethodButContains204Response() {
|
||||
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
};
|
||||
entry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo,
|
||||
Method.GET, "/", null,
|
||||
HttpStatus.SC_OK, headers,
|
||||
HttpTestUtils.makeNullResource());
|
||||
entry = makeEntry(Method.GET, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -308,33 +435,26 @@ public class TestCachedResponseSuitabilityChecker {
|
|||
final HttpRequest headRequest = new BasicHttpRequest("HEAD", "/foo");
|
||||
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom().build());
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
|
||||
|
||||
};
|
||||
entry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo,
|
||||
Method.GET, "/", null,
|
||||
HttpStatus.SC_OK, headers,
|
||||
HttpTestUtils.makeNullResource());
|
||||
entry = makeEntry(Method.GET, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
|
||||
Assertions.assertTrue(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, headRequest, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.FRESH, impl.assessSuitability(requestCacheControl, responseCacheControl, headRequest, entry, now));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotSuitableIfGetRequestWithHeadCacheEntry() {
|
||||
// Prepare a cache entry with HEAD method
|
||||
final Header[] headers = {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
|
||||
};
|
||||
entry = HttpTestUtils.makeCacheEntry(elevenSecondsAgo, nineSecondsAgo,
|
||||
Method.HEAD, "/", null,
|
||||
HttpStatus.SC_OK, headers,
|
||||
HttpTestUtils.makeNullResource());
|
||||
entry = makeEntry(Method.HEAD, "/foo",
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
|
||||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setMaxAge(3600)
|
||||
.build();
|
||||
// Validate that the cache entry is not suitable for the GET request
|
||||
Assertions.assertFalse(impl.canCachedResponseBeUsed(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
Assertions.assertEquals(CacheSuitability.MISMATCH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.apache.hc.core5.http.HttpException;
|
|||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.apache.hc.core5.http.Method;
|
||||
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
|
||||
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
|
||||
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
|
||||
|
@ -102,7 +103,7 @@ public class TestProtocolRequirements {
|
|||
|
||||
body = HttpTestUtils.makeBody(ENTITY_LENGTH);
|
||||
|
||||
request = new BasicClassicHttpRequest("GET", "/foo");
|
||||
request = new BasicClassicHttpRequest("GET", "/");
|
||||
|
||||
context = HttpClientContext.create();
|
||||
|
||||
|
@ -537,37 +538,6 @@ public class TestProtocolRequirements {
|
|||
Assertions.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses() throws Exception {
|
||||
|
||||
originResponse.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
|
||||
originResponse.setHeader("Content-Length", "128");
|
||||
originResponse.setHeader("Cache-Control", "max-age=3600");
|
||||
|
||||
final ClassicHttpRequest rangeReq = new BasicClassicHttpRequest("GET", "/");
|
||||
rangeReq.setHeader("Range", "bytes=1000-1200");
|
||||
|
||||
final ClassicHttpResponse orig416 = new BasicClassicHttpResponse(416,
|
||||
"Requested Range Not Satisfiable");
|
||||
|
||||
// cache may 416 me right away if it understands byte ranges,
|
||||
// ok to delegate to origin though
|
||||
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
|
||||
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(rangeReq), Mockito.any())).thenReturn(orig416);
|
||||
|
||||
execute(request);
|
||||
final ClassicHttpResponse result = execute(rangeReq);
|
||||
|
||||
// might have gotten a 416 from the origin or the cache
|
||||
Assertions.assertEquals(416, result.getCode());
|
||||
final Iterator<HeaderElement> it = MessageSupport.iterate(result, HttpHeaders.CONTENT_TYPE);
|
||||
while (it.hasNext()) {
|
||||
final HeaderElement elt = it.next();
|
||||
Assertions.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
|
||||
}
|
||||
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
|
||||
|
||||
|
@ -576,17 +546,12 @@ public class TestProtocolRequirements {
|
|||
final Instant nineSecondsAgo = now.minusSeconds(9);
|
||||
final Instant eightSecondsAgo = now.minusSeconds(8);
|
||||
|
||||
final Header[] hdrs = new Header[] {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
|
||||
new BasicHeader("Cache-Control", "max-age=0"),
|
||||
new BasicHeader("ETag", "\"etag\""),
|
||||
new BasicHeader("Content-Length", "128")
|
||||
};
|
||||
|
||||
final byte[] bytes = new byte[128];
|
||||
new Random().nextBytes(bytes);
|
||||
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
|
||||
Method.GET, "/thing", null,
|
||||
200, new Header[] {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(nineSecondsAgo)),
|
||||
new BasicHeader("ETag", "\"etag\"")
|
||||
}, HttpTestUtils.makeNullResource());
|
||||
|
||||
impl = new CachingExec(mockCache, null, config);
|
||||
|
||||
|
@ -602,6 +567,12 @@ public class TestProtocolRequirements {
|
|||
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
|
||||
new CacheMatch(new CacheHit("key", entry), null));
|
||||
Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(validate), Mockito.any())).thenReturn(notModified);
|
||||
final HttpCacheEntry updated = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
|
||||
Method.GET, "/thing", null,
|
||||
200, new Header[] {
|
||||
new BasicHeader("Date", DateUtils.formatStandardDate(now)),
|
||||
new BasicHeader("ETag", "\"etag\"")
|
||||
}, HttpTestUtils.makeNullResource());
|
||||
Mockito.when(mockCache.update(
|
||||
Mockito.any(),
|
||||
Mockito.any(),
|
||||
|
@ -609,7 +580,7 @@ public class TestProtocolRequirements {
|
|||
Mockito.any(),
|
||||
Mockito.any(),
|
||||
Mockito.any()))
|
||||
.thenReturn(new CacheHit("key", HttpTestUtils.makeCacheEntry()));
|
||||
.thenReturn(new CacheHit("key", updated));
|
||||
|
||||
execute(request);
|
||||
|
||||
|
@ -672,7 +643,7 @@ public class TestProtocolRequirements {
|
|||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, hdrs, bytes);
|
||||
|
||||
impl = new CachingExec(mockCache, null, config);
|
||||
request = new BasicClassicHttpRequest("GET", "/thing");
|
||||
request = new BasicClassicHttpRequest("GET", "/");
|
||||
|
||||
Mockito.when(mockCache.match(Mockito.eq(host), RequestEquivalent.eq(request))).thenReturn(
|
||||
new CacheMatch(new CacheHit("key", entry), null));
|
||||
|
|
Loading…
Reference in New Issue