Skip to content

Commit

Permalink
HTTPCLIENT-2277: Aligned CachedResponseSuitabilityChecker with the sp…
Browse files Browse the repository at this point in the history
…ecification requirements per RFC 9111 section 4
  • Loading branch information
ok2c committed Nov 3, 2023
1 parent c19cfe3 commit 7fbf29b
Show file tree
Hide file tree
Showing 13 changed files with 652 additions and 548 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,13 @@ public Iterator<Header> requestHeaderIterator() {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ public void execute(
}

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>() {
Expand All @@ -242,6 +245,9 @@ public void completed(final CacheMatch result) {
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);
}
}
Expand Down Expand Up @@ -590,21 +596,11 @@ private void handleCacheHit(
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);
Expand All @@ -623,60 +619,66 @@ private void handleCacheHit(
}
}
}
} 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)) {
final SimpleHttpResponse cacheResponse = generateGatewayTimeout(context);
triggerResponse(cacheResponse, scope, asyncExecCallback);
return;
}
if (cacheSuitability == CacheSuitability.MISMATCH) {
LOG.debug("Cache entry does not match the request; calling backend");
callBackend(target, request, entityProducer, scope, chain, 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("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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.core5.annotation.Contract;
Expand Down Expand Up @@ -201,6 +202,21 @@ public static List<String> variantNames(final MessageHeaders message) {
return names;
}

@Internal
public static void normalizeTokens(final Iterator<Header> iterator, final Consumer<String> consumer) {
final List<String> tokens = new ArrayList<>();
while (iterator.hasNext()) {
final Header header = iterator.next();
CacheSupport.parseTokens(header, tokens::add);
}
tokens.stream()
.filter(t -> !t.isEmpty())
.map(t -> t.toLowerCase(Locale.ROOT))
.sorted()
.distinct()
.forEach(consumer);
}

/**
* Computes a "variant key" for the given request and the given variants.
* @param request originating request
Expand All @@ -222,24 +238,13 @@ public String generateVariantKey(final HttpRequest request, final Collection<Str
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));
});
normalizeTokens(request.headerIterator(h), t -> {
if (!firstToken.compareAndSet(false, true)) {
buf.append(",");
}
buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
});
});
buf.append("}");
return buf.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* ====================================================================
* 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
NO_STORE // The cache entry must not be stored or must be immediately removed
// from the persistent storage.

}
Loading

0 comments on commit 7fbf29b

Please sign in to comment.