HTTPCLIENT-2277: Revised cache validation logic for conformance with the specification requirements per RFC 9111 section 4

This commit is contained in:
Oleg Kalnichevski 2023-11-09 20:43:54 +01:00
parent ebae9ef7e3
commit abb958ec27
12 changed files with 401 additions and 447 deletions

View File

@ -37,6 +37,7 @@ import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.async.AsyncExecCallback;
@ -168,12 +169,12 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
static class AsyncExecCallbackWrapper implements AsyncExecCallback {
private final AsyncExecCallback asyncExecCallback;
private final Runnable command;
private final Consumer<Exception> exceptionConsumer;
AsyncExecCallbackWrapper(final AsyncExecCallback asyncExecCallback, final Runnable command) {
this.asyncExecCallback = asyncExecCallback;
AsyncExecCallbackWrapper(final Runnable command, final Consumer<Exception> exceptionConsumer) {
this.command = command;
this.exceptionConsumer = exceptionConsumer;
}
@Override
@ -194,7 +195,9 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
@Override
public void failed(final Exception cause) {
asyncExecCallback.failed(cause);
if (exceptionConsumer != null) {
exceptionConsumer.accept(cause);
}
}
}
@ -603,7 +606,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
LOG.debug("Cache hit");
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(hit.entry, request, context);
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
recordCacheFailure(target, request);
@ -631,17 +634,47 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
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");
LOG.debug("Non-modified cache entry 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)) {
} else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
LOG.debug("Revalidation required; revalidating cache entry");
revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
private final AtomicBoolean committed = new AtomicBoolean();
@Override
public AsyncDataConsumer handleResponse(final HttpResponse response,
final EntityDetails entityDetails) throws HttpException, IOException {
committed.set(true);
return asyncExecCallback.handleResponse(response, entityDetails);
}
@Override
public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
asyncExecCallback.handleInformationResponse(response);
}
@Override
public void completed() {
asyncExecCallback.completed();
}
@Override
public void failed(final Exception cause) {
if (!committed.get() && cause instanceof IOException) {
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(scope.clientContext);
LOG.debug(cause.getMessage(), cause);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
asyncExecCallback.failed(cause);
}
}
});
} else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
if (cacheRevalidator != null) {
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(
@ -656,30 +689,19 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
cacheRevalidator.revalidateCacheEntry(
hit.getEntryKey(),
asyncExecCallback,
asyncExecCallback1 -> revalidateCacheEntry(requestCacheControl, responseCacheControl,
hit, target, request, entityProducer, fork, chain, asyncExecCallback1));
c -> revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, fork, chain, c));
final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, context, hit.entry);
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) {
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);
}
} catch (final IOException ex) {
asyncExecCallback.failed(ex);
}
} else {
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
LOG.debug("Revalidating stale cache entry (asynchronous revalidation disabled)");
revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
}
} else if (cacheSuitability == CacheSuitability.STALE) {
LOG.debug("Revalidating stale cache entry");
revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
} else {
LOG.debug("Cache entry not usable; calling backend");
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
@ -688,7 +710,6 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}
void revalidateCacheEntry(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
@ -720,17 +741,11 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
@Override
public void completed(final CacheHit updated) {
if (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, updated.entry, Instant.now())) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(updated.entry);
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, scope.clientContext, updated.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
}
} catch (final ResourceIOException ex) {
asyncExecCallback.failed(ex);
}
}
@ -762,12 +777,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
recordCacheUpdate(scope.clientContext);
}
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
return new AsyncExecCallbackWrapper(asyncExecCallback, () -> triggerUpdatedCacheEntryResponse(backendResponse, responseDate));
}
if (staleIfErrorAppliesTo(statusCode)
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, hit.entry, responseDate)) {
return new AsyncExecCallbackWrapper(asyncExecCallback, this::triggerResponseStaleCacheEntry);
return new AsyncExecCallbackWrapper(() -> triggerUpdatedCacheEntryResponse(backendResponse, responseDate), asyncExecCallback::failed);
}
return new BackendResponseHandler(target, conditionalRequest, requestDate, responseDate, scope, asyncExecCallback);
}
@ -780,12 +790,12 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final Instant responseDate1 = getCurrentDate();
final AsyncExecCallback callback1;
if (revalidationResponseIsTooOld(backendResponse1, hit.entry)) {
if (HttpCacheEntry.isNewer(hit.entry, backendResponse1)) {
final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
BasicRequestBuilder.copy(scope.originalRequest).build());
callback1 = new AsyncExecCallbackWrapper(asyncExecCallback, () -> chainProceed(unconditional, entityProducer, scope, chain, new AsyncExecCallback() {
callback1 = new AsyncExecCallbackWrapper(() -> chainProceed(unconditional, entityProducer, scope, chain, new AsyncExecCallback() {
@Override
public AsyncDataConsumer handleResponse(
@ -827,7 +837,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}
}
}));
}), asyncExecCallback::failed);
} else {
callback1 = evaluateResponse(backendResponse1, responseDate1);
}
@ -869,6 +879,71 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}
void revalidateCacheEntryWithFallback(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback asyncExecCallback) {
revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
private final AtomicBoolean committed = new AtomicBoolean();
@Override
public AsyncDataConsumer handleResponse(final HttpResponse response, final EntityDetails entityDetails) throws HttpException, IOException {
final int status = response.getCode();
if (staleIfErrorAppliesTo(status) &&
suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
return null;
} else {
committed.set(true);
return asyncExecCallback.handleResponse(response, entityDetails);
}
}
@Override
public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
asyncExecCallback.handleInformationResponse(response);
}
@Override
public void completed() {
if (committed.get()) {
asyncExecCallback.completed();
} else {
try {
final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, scope.clientContext, hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final IOException ex) {
asyncExecCallback.failed(ex);
}
}
}
@Override
public void failed(final Exception cause) {
if (!committed.get() &&
cause instanceof IOException &&
suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
try {
final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, scope.clientContext, hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final IOException ex) {
asyncExecCallback.failed(cause);
}
} else {
LOG.debug(cause.getMessage(), cause);
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(scope.clientContext);
triggerResponse(cacheResponse, scope, asyncExecCallback);
}
}
});
}
private void handleCacheMiss(
final RequestCacheControl requestCacheControl,
final CacheHit partialMatch,
@ -954,7 +1029,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
@Override
public void completed(final CacheHit hit) {
if (shouldSendNotModifiedResponse(request, hit.entry)) {
if (shouldSendNotModifiedResponse(request, hit.entry, Instant.now())) {
final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
@ -1055,21 +1130,21 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG);
if (resultEtagHeader == null) {
LOG.warn("304 response did not contain ETag");
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
} else {
final String resultEtag = resultEtagHeader.getValue();
final CacheHit match = variantMap.get(resultEtag);
if (match == null) {
LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
} else {
if (revalidationResponseIsTooOld(backendResponse, match.entry)) {
if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
BasicRequestBuilder.copy(request).build());
scope.clientContext.setAttribute(HttpCoreContext.HTTP_REQUEST, unconditional);
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback));
callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
} else {
callback = new AsyncExecCallbackWrapper(asyncExecCallback, () -> updateVariantCacheEntry(backendResponse, responseDate, match));
callback = new AsyncExecCallbackWrapper(() -> updateVariantCacheEntry(backendResponse, responseDate, match), asyncExecCallback::failed);
}
}
}

View File

@ -37,6 +37,8 @@ enum CacheSuitability {
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
STALE_WHILE_REVALIDATED, // the cache entry is stale but may be unsuitable to satisfy the request
// while being re-validated at the same time
REVALIDATION_REQUIRED
// the cache entry is stale and must not be used to satisfy the request
// without revalidation

View File

@ -146,34 +146,6 @@ class CacheValidityPolicy {
return heuristicDefaultLifetime;
}
public boolean isRevalidatable(final HttpCacheEntry entry) {
return entry.containsHeader(HttpHeaders.ETAG) || entry.containsHeader(HttpHeaders.LAST_MODIFIED);
}
public boolean mayReturnStaleWhileRevalidating(final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry, final Instant now) {
if (responseCacheControl.getStaleWhileRevalidate() >= 0) {
final TimeValue staleness = getStaleness(responseCacheControl, entry, now);
if (staleness.compareTo(TimeValue.ofSeconds(responseCacheControl.getStaleWhileRevalidate())) <= 0) {
return true;
}
}
return false;
}
public boolean mayReturnStaleIfError(final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry,
final Instant now) {
final TimeValue staleness = getStaleness(responseCacheControl, entry, now);
return mayReturnStaleIfError(requestCacheControl, staleness) ||
mayReturnStaleIfError(responseCacheControl, staleness);
}
private boolean mayReturnStaleIfError(final CacheControl responseCacheControl, final TimeValue staleness) {
return responseCacheControl.getStaleIfError() >= 0 &&
staleness.compareTo(TimeValue.ofSeconds(responseCacheControl.getStaleIfError())) <= 0;
}
TimeValue getApparentAge(final HttpCacheEntry entry) {
final Instant dateValue = entry.getInstant();
if (dateValue == null) {
@ -238,13 +210,4 @@ class CacheValidityPolicy {
return TimeValue.ofSeconds(diff.getSeconds());
}
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) {
return TimeValue.ZERO_MILLISECONDS;
}
return TimeValue.ofSeconds(age.toSeconds() - freshness.toSeconds());
}
}

View File

@ -58,14 +58,16 @@ class CachedResponseSuitabilityChecker {
private static final Logger LOG = LoggerFactory.getLogger(CachedResponseSuitabilityChecker.class);
private final boolean sharedCache;
private final CacheValidityPolicy validityStrategy;
private final boolean sharedCache;
private final boolean staleifError;
CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
final CacheConfig config) {
super();
this.validityStrategy = validityStrategy;
this.sharedCache = config.isSharedCache();
this.staleifError = config.isStaleIfErrorEnabled();
}
CachedResponseSuitabilityChecker(final CacheConfig config) {
@ -173,6 +175,13 @@ class CachedResponseSuitabilityChecker {
LOG.debug("The cache entry is fresh");
return CacheSuitability.FRESH;
} else {
if (responseCacheControl.getStaleWhileRevalidate() > 0) {
final long stale = currentAge.compareTo(freshnessLifetime) > 0 ? currentAge.toSeconds() - freshnessLifetime.toSeconds() : 0;
if (stale < responseCacheControl.getStaleWhileRevalidate()) {
LOG.debug("The cache entry is stale but suitable while being revalidated");
return CacheSuitability.STALE_WHILE_REVALIDATED;
}
}
LOG.debug("The cache entry is stale");
return CacheSuitability.STALE;
}
@ -349,4 +358,30 @@ class CachedResponseSuitabilityChecker {
return true;
}
public boolean isSuitableIfError(final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final Instant now) {
// Explicit cache control
if (requestCacheControl.getStaleIfError() > 0 || responseCacheControl.getStaleIfError() > 0) {
final TimeValue currentAge = validityStrategy.getCurrentAge(entry, now);
final TimeValue freshnessLifetime = validityStrategy.getFreshnessLifetime(responseCacheControl, entry);
if (requestCacheControl.getMinFresh() > 0 && requestCacheControl.getMinFresh() < freshnessLifetime.toSeconds()) {
return false;
}
final long stale = currentAge.compareTo(freshnessLifetime) > 0 ? currentAge.toSeconds() - freshnessLifetime.toSeconds() : 0;
if (requestCacheControl.getStaleIfError() > 0 && stale < requestCacheControl.getStaleIfError()) {
return true;
}
if (responseCacheControl.getStaleIfError() > 0 && stale < responseCacheControl.getStaleIfError()) {
return true;
}
}
// Global override
if (staleifError && requestCacheControl.getStaleIfError() == -1 && responseCacheControl.getStaleIfError() == -1) {
return true;
}
return false;
}
}

View File

@ -59,6 +59,7 @@ import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@ -251,7 +252,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
LOG.debug("Cache hit");
try {
return convert(generateCachedResponse(hit.entry, request, context), scope);
return convert(generateCachedResponse(request, context, hit.entry), scope);
} catch (final ResourceIOException ex) {
recordCacheFailure(target, request);
if (!mayCallBackend(requestCacheControl)) {
@ -271,40 +272,39 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
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");
LOG.debug("Non-modified cache entry 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);
} else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
LOG.debug("Revalidation required; revalidating cache 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);
return revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
} catch (final IOException ex) {
LOG.debug(ex.getMessage(), ex);
return convert(generateGatewayTimeout(scope.clientContext), scope);
}
} else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
if (cacheRevalidator != null) {
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());
cacheRevalidator.revalidateCacheEntry(
hit.getEntryKey(),
() -> revalidateCacheEntry(responseCacheControl, hit, target, request, fork, chain));
final SimpleHttpResponse response = unvalidatedCacheHit(request, context, hit.entry);
return convert(response, scope);
} else {
LOG.debug("Revalidating stale cache entry (asynchronous revalidation disabled)");
return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
}
} else if (cacheSuitability == CacheSuitability.STALE) {
LOG.debug("Revalidating stale cache entry");
return revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain);
} else {
LOG.debug("Cache entry not usable; calling backend");
return callBackend(target, request, scope, chain);
@ -313,7 +313,6 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
ClassicHttpResponse revalidateCacheEntry(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
@ -328,7 +327,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
try {
Instant responseDate = getCurrentDate();
if (revalidationResponseIsTooOld(backendResponse, hit.entry)) {
if (HttpCacheEntry.isNewer(hit.entry, backendResponse)) {
backendResponse.close();
final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
scope.originalRequest);
@ -341,25 +340,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
recordCacheUpdate(scope.clientContext);
}
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
final CacheHit updated = responseCache.update(hit, target, request, backendResponse, requestDate, responseDate);
if (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, updated.entry, Instant.now())) {
return convert(responseGenerator.generateNotModifiedResponse(updated.entry), scope);
}
return convert(responseGenerator.generateResponse(request, updated.entry), scope);
}
if (staleIfErrorAppliesTo(statusCode)
&& !staleResponseNotAllowed(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())
&& validityPolicy.mayReturnStaleIfError(requestCacheControl, responseCacheControl, hit.entry, responseDate)) {
try {
final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, hit.entry);
return convert(cachedResponse, scope);
} finally {
backendResponse.close();
}
return convert(generateCachedResponse(request, scope.clientContext, updated.entry), scope);
}
return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
} catch (final IOException | RuntimeException ex) {
@ -368,6 +351,41 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
}
}
ClassicHttpResponse revalidateCacheEntryWithFallback(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final CacheHit hit,
final HttpHost target,
final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain) throws HttpException, IOException {
final HttpClientContext context = scope.clientContext;
final ClassicHttpResponse response;
try {
response = revalidateCacheEntry(responseCacheControl, hit, target, request, scope, chain);
} catch (final IOException ex) {
if (suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
}
return convert(unvalidatedCacheHit(request, context, hit.entry), scope);
} else {
LOG.debug(ex.getMessage(), ex);
return convert(generateGatewayTimeout(context), scope);
}
}
final int status = response.getCode();
if (staleIfErrorAppliesTo(status) &&
suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Serving stale response due to {} status and stale-if-error enabled", status);
}
EntityUtils.consume(response.getEntity());
return convert(unvalidatedCacheHit(request, context, hit.entry), scope);
}
return response;
}
ClassicHttpResponse handleBackendResponse(
final HttpHost target,
final ClassicHttpRequest request,
@ -518,7 +536,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
return callBackend(target, request, scope, chain);
}
if (revalidationResponseIsTooOld(backendResponse, match.entry)) {
if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
return callBackend(target, unconditional, scope, chain);
}
@ -526,7 +544,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
recordCacheUpdate(scope.clientContext);
final CacheHit hit = responseCache.storeFromNegotiated(match, target, request, backendResponse, requestDate, responseDate);
if (shouldSendNotModifiedResponse(request, hit.entry)) {
if (shouldSendNotModifiedResponse(request, hit.entry, Instant.now())) {
return convert(responseGenerator.generateNotModifiedResponse(hit.entry), scope);
} else {
return convert(responseGenerator.generateResponse(request, hit.entry), scope);

View File

@ -44,7 +44,6 @@ import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.Method;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.TimeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -88,8 +87,7 @@ public class CachingExecBase {
this.responseCachingPolicy = new ResponseCachingPolicy(
this.cacheConfig.isSharedCache(),
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery(),
this.cacheConfig.isStaleIfErrorEnabled());
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery());
}
/**
@ -146,31 +144,14 @@ public class CachingExecBase {
}
SimpleHttpResponse generateCachedResponse(
final HttpCacheEntry entry,
final HttpRequest request,
final HttpContext context) throws ResourceIOException {
final SimpleHttpResponse cachedResponse;
if (request.containsHeader(HttpHeaders.IF_NONE_MATCH)
|| request.containsHeader(HttpHeaders.IF_MODIFIED_SINCE)) {
cachedResponse = responseGenerator.generateNotModifiedResponse(entry);
} else {
cachedResponse = responseGenerator.generateResponse(request, entry);
}
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
return cachedResponse;
}
SimpleHttpResponse handleRevalidationFailure(
final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final HttpRequest request,
final HttpContext context,
final Instant now) throws IOException {
if (staleResponseNotAllowed(requestCacheControl, responseCacheControl, entry, now)) {
return generateGatewayTimeout(context);
final HttpCacheEntry entry) throws ResourceIOException {
setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
if (shouldSendNotModifiedResponse(request, entry, Instant.now())) {
return responseGenerator.generateNotModifiedResponse(entry);
} else {
return unvalidatedCacheHit(request, context, entry);
return responseGenerator.generateResponse(request, entry);
}
}
@ -189,15 +170,6 @@ public class CachingExecBase {
return cachedResponse;
}
boolean staleResponseNotAllowed(final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final Instant now) {
return responseCacheControl.isMustRevalidate()
|| (cacheConfig.isSharedCache() && responseCacheControl.isProxyRevalidate())
|| explicitFreshnessRequest(requestCacheControl, responseCacheControl, entry, now);
}
boolean mayCallBackend(final RequestCacheControl requestCacheControl) {
if (requestCacheControl.isOnlyIfCached()) {
LOG.debug("Request marked only-if-cached");
@ -206,22 +178,6 @@ public class CachingExecBase {
return true;
}
boolean explicitFreshnessRequest(final RequestCacheControl requestCacheControl,
final ResponseCacheControl responseCacheControl,
final HttpCacheEntry entry,
final Instant now) {
if (requestCacheControl.getMaxStale() >= 0) {
final TimeValue age = validityPolicy.getCurrentAge(entry, now);
final TimeValue lifetime = validityPolicy.getFreshnessLifetime(responseCacheControl, entry);
if (age.toSeconds() - lifetime.toSeconds() > requestCacheControl.getMaxStale()) {
return true;
}
} else if (requestCacheControl.getMinFresh() >= 0 || requestCacheControl.getMaxAge() >= 0) {
return true;
}
return false;
}
void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
if (context != null) {
context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, value);
@ -245,17 +201,9 @@ public class CachingExecBase {
return "0".equals(h != null ? h.getValue() : null);
}
boolean revalidationResponseIsTooOld(final HttpResponse backendResponse, final HttpCacheEntry cacheEntry) {
// either backend response or cached entry did not have a valid
// Date header, so we can't tell if they are out of order
// according to the origin clock; thus we can skip the
// unconditional retry.
return HttpCacheEntry.isNewer(cacheEntry, backendResponse);
}
boolean shouldSendNotModifiedResponse(final HttpRequest request, final HttpCacheEntry responseEntry) {
return (suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, responseEntry, Instant.now()));
boolean shouldSendNotModifiedResponse(final HttpRequest request, final HttpCacheEntry responseEntry, final Instant now) {
return suitabilityChecker.isConditional(request)
&& suitabilityChecker.allConditionalsMatch(request, responseEntry, now);
}
boolean staleIfErrorAppliesTo(final int statusCode) {

View File

@ -30,7 +30,6 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpRequest;
@ -63,14 +62,6 @@ class ResponseCachingPolicy {
private final boolean neverCache1_0ResponsesWithQueryString;
private final boolean neverCache1_1ResponsesWithQueryString;
/**
* A flag indicating whether serving stale cache entries is allowed when an error occurs
* while fetching a fresh response from the origin server.
* If {@code true}, stale cache entries may be served in case of errors.
* If {@code false}, stale cache entries will not be served in case of errors.
*/
private final boolean staleIfErrorEnabled;
/**
* Constructs a new ResponseCachingPolicy with the specified cache policy settings and stale-if-error support.
*
@ -80,20 +71,15 @@ class ResponseCachingPolicy {
* {@code false} to cache if explicit cache headers are found.
* @param neverCache1_1ResponsesWithQueryString {@code true} to never cache HTTP 1.1 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
* @param staleIfErrorEnabled {@code true} to enable the stale-if-error cache directive, which
* allows clients to receive a stale cache entry when a request
* results in an error, {@code false} to disable this feature.
* @since 5.3
*/
public ResponseCachingPolicy(
final boolean sharedCache,
final boolean neverCache1_0ResponsesWithQueryString,
final boolean neverCache1_1ResponsesWithQueryString,
final boolean staleIfErrorEnabled) {
final boolean neverCache1_1ResponsesWithQueryString) {
this.sharedCache = sharedCache;
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
this.neverCache1_1ResponsesWithQueryString = neverCache1_1ResponsesWithQueryString;
this.staleIfErrorEnabled = staleIfErrorEnabled;
}
/**
@ -374,28 +360,6 @@ class ResponseCachingPolicy {
return DEFAULT_FRESHNESS_DURATION; // 5 minutes
}
/**
* Determines whether a stale response should be served in case of an error status code in the cached response.
* This method first checks if the {@code stale-if-error} extension is enabled in the cache configuration. If it is, it
* then checks if the cached response has an error status code (500-504). If it does, it checks if the response has a
* {@code stale-while-revalidate} directive in its Cache-Control header. If it does, this method returns {@code true},
* indicating that a stale response can be served. If not, it returns {@code false}.
*
* @return {@code true} if a stale response can be served in case of an error status code, {@code false} otherwise
*/
boolean isStaleIfErrorEnabled(final ResponseCacheControl cacheControl, final HttpCacheEntry entry) {
// Check if the stale-while-revalidate extension is enabled
if (staleIfErrorEnabled) {
// Check if the cached response has an error status code
final int statusCode = entry.getStatus();
if (statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR && statusCode <= HttpStatus.SC_GATEWAY_TIMEOUT) {
// Check if the cached response has a stale-while-revalidate directive
return cacheControl.getStaleWhileRevalidate() > 0;
}
}
return false;
}
/**
* Understood status codes include:
* - All 2xx (Successful) status codes (200-299)

View File

@ -28,7 +28,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.assertTrue;
import java.time.Instant;
@ -253,30 +252,6 @@ public class TestCacheValidityPolicy {
assertEquals(defaultFreshness, impl.getHeuristicFreshnessLifetime(entry));
}
@Test
public void testCacheEntryIsRevalidatableIfHeadersIncludeETag() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
new BasicHeader("ETag", "somevalue"));
assertTrue(impl.isRevalidatable(entry));
}
@Test
public void testCacheEntryIsRevalidatableIfHeadersIncludeLastModifiedDate() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
new BasicHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now())));
assertTrue(impl.isRevalidatable(entry));
}
@Test
public void testCacheEntryIsNotRevalidatableIfNoAppropriateHeaders() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
new BasicHeader("Expires", DateUtils.formatStandardDate(Instant.now())),
new BasicHeader("Cache-Control", "public"));
assertFalse(impl.isRevalidatable(entry));
}
@Test
public void testNegativeAgeHeaderValueReturnsZero() {
final Header[] headers = new Header[] { new BasicHeader("Age", "-100") };
@ -326,103 +301,4 @@ public class TestCacheValidityPolicy {
assertEquals(0, impl.getAgeValue(entry));
}
@Test
public void testMayReturnStaleIfErrorInResponseIsTrueWithinStaleness(){
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final RequestCacheControl requestCacheControl = RequestCacheControl.builder()
.build();
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(15)
.build();
assertTrue(impl.mayReturnStaleIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testMayReturnStaleIfErrorInRequestIsTrueWithinStaleness(){
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final RequestCacheControl requestCacheControl = RequestCacheControl.builder()
.build();
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setStaleIfError(15)
.build();
assertTrue(impl.mayReturnStaleIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testMayNotReturnStaleIfErrorInResponseAndAfterResponseWindow(){
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final RequestCacheControl requestCacheControl = RequestCacheControl.builder()
.build();
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(1)
.build();
assertFalse(impl.mayReturnStaleIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testMayNotReturnStaleIfErrorInResponseAndAfterRequestWindow(){
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
new BasicHeader("Cache-Control", "max-age=5"));
final RequestCacheControl requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(1)
.build();
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
assertFalse(impl.mayReturnStaleIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testMayReturnStaleWhileRevalidatingIsFalseWhenDirectiveIsAbsent() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setCachePublic(true)
.build();
assertFalse(impl.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now));
}
@Test
public void testMayReturnStaleWhileRevalidatingIsTrueWhenWithinStaleness() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleWhileRevalidate(15)
.build();
assertTrue(impl.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now));
}
@Test
public void testMayReturnStaleWhileRevalidatingIsFalseWhenPastStaleness() {
final Instant twentyFiveSecondsAgo = now.minusSeconds(25);
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(twentyFiveSecondsAgo)));
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleWhileRevalidate(15)
.build();
assertFalse(impl.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now));
}
@Test
public void testMayReturnStaleWhileRevalidatingIsFalseWhenDirectiveEmpty() {
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
final ResponseCacheControl responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleWhileRevalidate(0)
.build();
assertFalse(impl.mayReturnStaleWhileRevalidating(responseCacheControl, entry, now));
}
}

View File

@ -457,4 +457,105 @@ public class TestCachedResponseSuitabilityChecker {
// Validate that the cache entry is not suitable for the GET request
Assertions.assertEquals(CacheSuitability.MISMATCH, impl.assessSuitability(requestCacheControl, responseCacheControl, request, entry, now));
}
@Test
public void testSuitableIfErrorRequestCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
// the entry has been stale for 6 seconds
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.build();
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.setMinFresh(4) // should take precedence over stale-if-error
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(-1) // not set or not valid
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorResponseCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(10)
.build();
// the entry has been stale for 6 seconds
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(-1) // not set or not valid
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorRequestCacheControlTakesPrecedenceOverResponseCacheControl() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.setStaleIfError(5)
.build();
// the entry has been stale for 6 seconds
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(10)
.build();
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
@Test
public void testSuitableIfErrorConfigDefault() {
// Prepare a cache entry with HEAD method
entry = makeEntry(Method.GET, "/foo",
new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(5)
.build();
impl = new CachedResponseSuitabilityChecker(CacheConfig.custom()
.setStaleIfErrorEnabled(true)
.build());
Assertions.assertTrue(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
requestCacheControl = RequestCacheControl.builder()
.setStaleIfError(5)
.build();
Assertions.assertFalse(impl.isSuitableIfError(requestCacheControl, responseCacheControl, entry, now));
}
}

View File

@ -589,7 +589,7 @@ public class TestCachingExecChain {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new IOException());
execute(req2);
Assertions.assertEquals(CacheResponseStatus.CACHE_HIT, context.getCacheResponseStatus());
Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, context.getCacheResponseStatus());
}
@Test
@ -1220,33 +1220,31 @@ public class TestCachingExecChain {
// Create the first request and response
final BasicClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
final BasicClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "OK");
final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
resp1.setEntity(HttpTestUtils.makeBody(128));
resp1.setHeader("Content-Length", "128");
resp1.setHeader("ETag", "\"etag\"");
resp1.setHeader("ETag", "\"abc\"");
resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now().minus(Duration.ofHours(10))));
resp1.setHeader("Cache-Control", "public, max-age=-1, stale-while-revalidate=1");
resp1.setHeader("Cache-Control", "public, stale-while-revalidate=1");
final BasicClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "http://foo.example.com/");
req2.addHeader("If-None-Match", "\"abc\"");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
// Set up the mock response chain
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
// Execute the first request and assert the response
final ClassicHttpResponse response1 = execute(req1);
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, response1.getCode());
Assertions.assertEquals(HttpStatus.SC_OK, response1.getCode());
// Execute the second request and assert the response
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
final ClassicHttpResponse response2 = execute(req2);
Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, response2.getCode());
Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
// Assert that the cache revalidator was called
Mockito.verify(cacheRevalidator, Mockito.times(1)).revalidateCacheEntry(Mockito.any(), Mockito.any());
Mockito.verify(cacheRevalidator, Mockito.never()).revalidateCacheEntry(Mockito.any(), Mockito.any());
}
@Test

View File

@ -27,14 +27,11 @@
package org.apache.hc.client5.http.impl.cache;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.time.Instant;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpException;
@ -107,29 +104,6 @@ public class TestProtocolAllowedBehavior {
mockExecChain);
}
@Test
public void testNonSharedCacheReturnsStaleResponseWhenRevalidationFailsForProxyRevalidate() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
final Instant now = Instant.now();
final Instant tenSecondsAgo = now.minusSeconds(10);
originResponse.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
originResponse.setHeader("Cache-Control","max-age=5,proxy-revalidate");
originResponse.setHeader("Etag","\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET","/");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenThrow(new SocketTimeoutException());
final HttpResponse result = execute(req2);
Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
Mockito.verifyNoInteractions(mockCache);
}
@Test
public void testNonSharedCacheMayCacheResponsesWithCacheControlPrivate() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");

View File

@ -68,7 +68,7 @@ public class TestResponseCachingPolicy {
sixSecondsAgo = now.minusSeconds(6);
tenSecondsFromNow = now.plusSeconds(10);
policy = new ResponseCachingPolicy(true, false, false, false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET","/");
response = new BasicHttpResponse(HttpStatus.SC_OK, "");
response.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
@ -78,14 +78,14 @@ public class TestResponseCachingPolicy {
@Test
public void testGetCacheable() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest(Method.GET, "/");
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
@Test
public void testHeadCacheable() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest(Method.HEAD, "/");
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
@ -108,7 +108,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesToRequestsWithAuthorizationHeadersAreCacheableByNonSharedCache() {
policy = new ResponseCachingPolicy(false, false, false, true);
policy = new ResponseCachingPolicy(false, false, false);
request = new BasicHttpRequest("GET","/");
request.setHeader("Authorization", StandardAuthScheme.BASIC + " dXNlcjpwYXNzd2Q=");
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
@ -191,7 +191,7 @@ public class TestResponseCachingPolicy {
@Test
public void testPlain303ResponseCodeIsNotCacheableEvenIf303CachingEnabled() {
policy = new ResponseCachingPolicy(true, false, true, true);
policy = new ResponseCachingPolicy(true, false, true);
response.setCode(HttpStatus.SC_SEE_OTHER);
response.removeHeaders("Expires");
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
@ -283,7 +283,7 @@ public class TestResponseCachingPolicy {
@Test
public void test200ResponseWithPrivateCacheControlIsCacheableByNonSharedCache() {
policy = new ResponseCachingPolicy(false, false, false, true);
policy = new ResponseCachingPolicy(false, false, false);
response.setCode(HttpStatus.SC_OK);
responseCacheControl = ResponseCacheControl.builder()
.setCachePrivate(true)
@ -387,7 +387,7 @@ public class TestResponseCachingPolicy {
@Test
public void testVaryStarIsNotCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
response.setHeader("Vary", "*");
@ -405,7 +405,7 @@ public class TestResponseCachingPolicy {
@Test
public void testIsArbitraryMethodCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new HttpOptions("http://foo.example.com/");
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
@ -426,7 +426,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesWithMultipleAgeHeadersAreNotCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
response.addHeader("Age", "3");
@ -446,7 +446,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesWithMultipleDateHeadersAreNotCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
response.addHeader("Date", DateUtils.formatStandardDate(now));
@ -465,7 +465,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesWithMalformedDateHeadersAreNotCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
response.addHeader("Date", "garbage");
@ -484,7 +484,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesWithMultipleExpiresHeadersAreNotCacheableUsingSharedPublicCache() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request.setHeader("Authorization", StandardAuthScheme.BASIC + " QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
response.addHeader("Expires", DateUtils.formatStandardDate(now));
@ -515,14 +515,14 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesToGETWithQueryParamsButNoExplicitCachingAreNotCacheableEvenWhen1_0QueryCachingDisabled() {
policy = new ResponseCachingPolicy(true, true, false, false);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("GET", "/foo?s=bar");
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
}
@Test
public void testResponsesToHEADWithQueryParamsButNoExplicitCachingAreNotCacheableEvenWhen1_0QueryCachingDisabled() {
policy = new ResponseCachingPolicy(true, true, false, false);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
}
@ -537,7 +537,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesToHEADWithQueryParamsAndExplicitCachingAreCacheable() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -546,7 +546,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesToGETWithQueryParamsAndExplicitCachingAreCacheableEvenWhen1_0QueryCachingDisabled() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("GET", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -555,7 +555,7 @@ public class TestResponseCachingPolicy {
@Test
public void testResponsesToHEADWithQueryParamsAndExplicitCachingAreCacheableEvenWhen1_0QueryCachingDisabled() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -580,7 +580,7 @@ public class TestResponseCachingPolicy {
@Test
public void getsWithQueryParametersDirectlyFrom1_0OriginsAreNotCacheableEvenWithSetting() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("GET", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -589,7 +589,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersDirectlyFrom1_0OriginsAreNotCacheableEvenWithSetting() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -608,7 +608,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersDirectlyFrom1_0OriginsAreCacheableWithExpires() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -619,7 +619,7 @@ public class TestResponseCachingPolicy {
@Test
public void getsWithQueryParametersDirectlyFrom1_0OriginsCanBeNotCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("GET", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -630,7 +630,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersDirectlyFrom1_0OriginsCanBeNotCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, false, true);
policy = new ResponseCachingPolicy(true, true, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -664,7 +664,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersFrom1_0OriginsViaProxiesAreCacheableWithExpires() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -674,7 +674,7 @@ public class TestResponseCachingPolicy {
@Test
public void getsWithQueryParametersFrom1_0OriginsViaProxiesCanNotBeCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, true, true);
policy = new ResponseCachingPolicy(true, true, true);
request = new BasicHttpRequest("GET", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -684,7 +684,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersFrom1_0OriginsViaProxiesCanNotBeCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, true, true);
policy = new ResponseCachingPolicy(true, true, true);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -703,7 +703,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersFrom1_0OriginsViaExplicitProxiesAreCacheableWithExpires() {
policy = new ResponseCachingPolicy(true, false, false, false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -713,7 +713,7 @@ public class TestResponseCachingPolicy {
@Test
public void getsWithQueryParametersFrom1_0OriginsViaExplicitProxiesCanNotBeCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, true, true);
policy = new ResponseCachingPolicy(true, true, true);
request = new BasicHttpRequest("GET", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -723,7 +723,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersFrom1_0OriginsViaExplicitProxiesCanNotBeCacheableEvenWithExpires() {
policy = new ResponseCachingPolicy(true, true, true, true);
policy = new ResponseCachingPolicy(true, true, true);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response.setHeader("Date", DateUtils.formatStandardDate(now));
response.setHeader("Expires", DateUtils.formatStandardDate(tenSecondsFromNow));
@ -744,7 +744,7 @@ public class TestResponseCachingPolicy {
@Test
public void headsWithQueryParametersFrom1_1OriginsVia1_0ProxiesAreCacheableWithExpires() {
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("HEAD", "/foo?s=bar");
response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
response.setVersion(HttpVersion.HTTP_1_0);
@ -783,7 +783,7 @@ public class TestResponseCachingPolicy {
public void test303WithExplicitCachingHeadersWhenPermittedByConfig() {
// HTTPbis working group says ok if explicitly indicated by
// response headers
policy = new ResponseCachingPolicy(true, false, true, true);
policy = new ResponseCachingPolicy(true, false, true);
response.setCode(HttpStatus.SC_SEE_OTHER);
response.setHeader("Date", DateUtils.formatStandardDate(now));
responseCacheControl = ResponseCacheControl.builder()
@ -824,7 +824,7 @@ public class TestResponseCachingPolicy {
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false, false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
@ -842,7 +842,7 @@ public class TestResponseCachingPolicy {
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false, false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(60)
@ -862,7 +862,7 @@ public class TestResponseCachingPolicy {
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false,false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setMaxAge(60)
@ -882,7 +882,7 @@ public class TestResponseCachingPolicy {
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false,false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
@ -898,7 +898,7 @@ public class TestResponseCachingPolicy {
request = new BasicHttpRequest("GET","/foo?s=bar");
// HTTPbis working group says ok if explicitly indicated by
// response headers
policy = new ResponseCachingPolicy(true, false, true, true);
policy = new ResponseCachingPolicy(true, false, true);
response.setCode(HttpStatus.SC_OK);
response.setHeader("Date", DateUtils.formatStandardDate(now));
assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
@ -911,7 +911,7 @@ public class TestResponseCachingPolicy {
response.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false, true);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(true)
@ -926,7 +926,7 @@ public class TestResponseCachingPolicy {
response.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
// Create ResponseCachingPolicy instance and test the method
policy = new ResponseCachingPolicy(true, false, false, false);
policy = new ResponseCachingPolicy(true, false, false);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setNoStore(true)