Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.apache.hc.client5.http.cookie.CookieSpec;
import org.apache.hc.client5.http.cookie.CookieSpecFactory;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.cache.CacheStatus;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.protocol.RedirectLocations;
import org.apache.hc.core5.annotation.Internal;
Expand Down Expand Up @@ -73,6 +74,7 @@ public class HttpCacheContext extends HttpClientContext {
static final String REQUEST_CACHE_CONTROL = "http.cache.request-control";
static final String RESPONSE_CACHE_CONTROL = "http.cache.response-control";
static final String CACHE_ENTRY = "http.cache.entry";
static final String CACHE_STATUS = "http.cache.status";

/**
* @deprecated Use {@link #castOrCreate(HttpContext)}.
Expand Down Expand Up @@ -118,10 +120,10 @@ public static HttpCacheContext create() {
return new HttpCacheContext();
}

private CacheResponseStatus responseStatus;
private RequestCacheControl requestCacheControl;
private ResponseCacheControl responseCacheControl;
private HttpCacheEntry cacheEntry;
private CacheStatus cacheStatus;
Comment thread
arturobernalg marked this conversation as resolved.

public HttpCacheContext(final HttpContext context) {
super(context);
Expand All @@ -133,20 +135,12 @@ public HttpCacheContext() {

/**
* Represents an outcome of the cache operation and the way the response has been
* generated.
* <p>
* This context attribute is expected to be populated by the protocol handler.
* generated. The value is derived on the fly from the {@link CacheStatus} recorded for the
* exchange, which is the single source of truth for how the request was handled.
*/
public CacheResponseStatus getCacheResponseStatus() {
return responseStatus;
}

/**
* @since 5.4
*/
@Internal
public void setCacheResponseStatus(final CacheResponseStatus responseStatus) {
this.responseStatus = responseStatus;
final CacheStatus status = getCacheStatus();
return status != null ? status.toResponseStatus() : null;
}

/**
Expand Down Expand Up @@ -228,6 +222,25 @@ public void setCacheEntry(final HttpCacheEntry cacheEntry) {
this.cacheEntry = cacheEntry;
}

/**
* Records how the cache handled the current exchange, used to render the RFC 9211
* {@code Cache-Status} response header.
*
* @since 5.7
*/
@Internal
public CacheStatus getCacheStatus() {
return cacheStatus;
}

/**
* @since 5.7
*/
@Internal
public void setCacheStatus(final CacheStatus cacheStatus) {
this.cacheStatus = cacheStatus;
}

/**
* Internal adaptor class that delegates all its method calls to {@link HttpClientContext}.
* To be removed in the future.
Expand All @@ -243,16 +256,6 @@ static class Delegate extends HttpCacheContext {
this.clientContext = clientContext;
}

@Override
public CacheResponseStatus getCacheResponseStatus() {
return clientContext.getAttribute(CACHE_RESPONSE_STATUS, CacheResponseStatus.class);
}

@Override
public void setCacheResponseStatus(final CacheResponseStatus responseStatus) {
clientContext.setAttribute(CACHE_RESPONSE_STATUS, responseStatus);
}

@Override
public RequestCacheControl getRequestCacheControl() {
return clientContext.getAttribute(REQUEST_CACHE_CONTROL, RequestCacheControl.class);
Expand Down Expand Up @@ -283,6 +286,16 @@ public void setCacheEntry(final HttpCacheEntry cacheEntry) {
clientContext.setAttribute(CACHE_ENTRY, cacheEntry);
}

@Override
public CacheStatus getCacheStatus() {
return clientContext.getAttribute(CACHE_STATUS, CacheStatus.class);
}

@Override
public void setCacheStatus(final CacheStatus cacheStatus) {
clientContext.setAttribute(CACHE_STATUS, cacheStatus);
}

@Override
public RouteInfo getHttpRoute() {
return clientContext.getHttpRoute();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.cache.CacheResponseStatus;
import org.apache.hc.client5.http.cache.HttpCacheContext;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.RequestCacheControl;
Expand Down Expand Up @@ -215,6 +214,7 @@ public AsyncDataConsumer handleResponse(
final EntityDetails entityDetails) throws HttpException, IOException {
context.setRequest(request);
context.setResponse(response);
applyCacheStatus(response, HttpCacheContext.cast(context));
return asyncExecCallback.handleResponse(response, entityDetails);
}

Expand Down Expand Up @@ -253,11 +253,13 @@ public void doExecute(
LOG.debug("{} request via cache: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
}

context.setCacheResponseStatus(CacheResponseStatus.CACHE_MISS);
context.setCacheEntry(null);
final CacheStatus cacheStatus = new CacheStatus();
cacheStatus.forward(CacheStatus.ForwardReason.MISS);
context.setCacheStatus(cacheStatus);

if (clientRequestsOurOptions(request)) {
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
cacheStatus.suppress();
triggerResponse(SimpleHttpResponse.create(HttpStatus.SC_NOT_IMPLEMENTED), scope, asyncExecCallback);
return;
}
Expand All @@ -267,6 +269,12 @@ public void doExecute(
if (LOG.isDebugEnabled()) {
LOG.debug("{} request cannot be correctly executed and cached", exchangeId);
}
// The request could not be represented for caching. A cache-supported method whose
// body/representation prevents processing is a bypass; any other method is not handled
// by the cache at all.
cacheStatus.forward(CacheSupport.isMethodCacheSupported(request.getMethod())
? CacheStatus.ForwardReason.BYPASS
: CacheStatus.ForwardReason.METHOD);
chain.proceed(request, entityProducer, scope, asyncExecCallback);
return;
}
Expand All @@ -288,6 +296,11 @@ public void doExecute(
if (LOG.isDebugEnabled()) {
LOG.debug("{} request cannot be served from cache", exchangeId);
}
// The cache lookup is bypassed here (non-cacheable method, or a no-store / no-cache
// request), so no stored response was ever selected; this is not fwd=request.
cacheStatus.forward(CacheSupport.isMethodCacheSupported(cacheRequest.getMethod())
? CacheStatus.ForwardReason.BYPASS
: CacheStatus.ForwardReason.METHOD);
callChain(cacheRequest, scope, chain, asyncExecCallback);
return;
}
Expand Down Expand Up @@ -937,7 +950,6 @@ private void handleCacheHit(
LOG.debug("{} cache hit: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
}

context.setCacheResponseStatus(CacheResponseStatus.CACHE_HIT);
cacheHits.getAndIncrement();

final Instant now = getCurrentDate();
Expand All @@ -953,17 +965,18 @@ private void handleCacheHit(
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now);
context.setCacheEntry(hit.entry);
cacheStatus(context).hit();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final ResourceIOException ex) {
if (requestCacheControl.isOnlyIfCached()) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} request marked only-if-cached", exchangeId);
}
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
cacheStatus(context).suppress();
final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
context.setCacheResponseStatus(CacheResponseStatus.FAILURE);
cacheStatus(context).fail();
callChain(request, scope, chain, asyncExecCallback);
}
}
Expand All @@ -972,7 +985,7 @@ private void handleCacheHit(
if (LOG.isDebugEnabled()) {
LOG.debug("{} cache entry not is not fresh and only-if-cached requested", exchangeId);
}
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
cacheStatus(context).suppress();
final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else if (cacheSuitability == CacheSuitability.MISMATCH) {
Expand All @@ -989,6 +1002,16 @@ private void handleCacheHit(
if (LOG.isDebugEnabled()) {
LOG.debug("{} revalidation required; revalidating cache entry", exchangeId);
}
// fwd=request only when a still-fresh stored response could not be used because of
// request semantics; a stale entry that must be revalidated is fwd=stale.
final boolean stale = validityPolicy.getCurrentAge(hit.entry, now)
.compareTo(validityPolicy.getFreshnessLifetime(responseCacheControl, hit.entry)) >= 0;
final boolean requestForced = requestCacheControl.isNoCache()
|| requestCacheControl.getMaxAge() >= 0
|| requestCacheControl.getMinFresh() >= 0;
cacheStatus(context).forward(!stale && requestForced
? CacheStatus.ForwardReason.REQUEST
: CacheStatus.ForwardReason.STALE);
revalidateCacheEntryWithoutFallback(requestCacheControl, responseCacheControl, hit, target, request, scope, chain, asyncExecCallback);
} else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
if (cacheRevalidator != null) {
Expand All @@ -1014,9 +1037,10 @@ private void handleCacheHit(
hit.getEntryKey(),
asyncExecCallback,
c -> revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, fork, chain, c));
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
context.setCacheEntry(hit.entry);
cacheStatus(context).hit();
cacheStatus(context).moduleResponse();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final IOException ex) {
asyncExecCallback.failed(ex);
Expand Down Expand Up @@ -1098,7 +1122,7 @@ public void cancelled() {
AsyncExecCallback evaluateResponse(final HttpResponse backendResponse, final Instant responseDate) {
final int statusCode = backendResponse.getCode();
if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
cacheStatus(context).forwardStatus(statusCode);
cacheUpdates.getAndIncrement();
}
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
Expand Down Expand Up @@ -1241,7 +1265,7 @@ public void failed(final Exception cause) {
LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
}
final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
cacheStatus(context).suppress();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} else {
asyncExecCallback.failed(cause);
Expand All @@ -1262,6 +1286,7 @@ void revalidateCacheEntryWithFallback(
final AsyncExecCallback asyncExecCallback) {
final String exchangeId = scope.exchangeId;
final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
cacheStatus(context).forward(CacheStatus.ForwardReason.STALE);
revalidateCacheEntry(requestCacheControl, responseCacheControl, hit, target, request, scope, chain, new AsyncExecCallback() {

private final AtomicReference<HttpResponse> committed = new AtomicReference<>();
Expand All @@ -1274,6 +1299,8 @@ public AsyncDataConsumer handleResponse(final HttpResponse response, final Entit
if (LOG.isDebugEnabled()) {
LOG.debug("{} serving stale response due to {} status and stale-if-error enabled", exchangeId, status);
}
cacheStatus(context).forwardStatus(status);
cacheStatus(context).moduleResponse();
return null;
}
committed.set(response);
Expand All @@ -1290,7 +1317,6 @@ public void completed() {
final HttpResponse response = committed.get();
if (response == null) {
try {
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
context.setCacheEntry(hit.entry);
triggerResponse(cacheResponse, scope, asyncExecCallback);
Expand All @@ -1309,7 +1335,6 @@ public void failed(final Exception cause) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
}
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
if (cause instanceof IOException &&
suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
if (LOG.isDebugEnabled()) {
Expand All @@ -1318,11 +1343,13 @@ public void failed(final Exception cause) {
try {
final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
context.setCacheEntry(hit.entry);
cacheStatus(context).moduleResponse();
triggerResponse(cacheResponse, scope, asyncExecCallback);
} catch (final IOException ex) {
asyncExecCallback.failed(cause);
}
} else {
cacheStatus(context).suppress();
final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
triggerResponse(cacheResponse, scope, asyncExecCallback);
}
Expand Down Expand Up @@ -1354,7 +1381,7 @@ private void handleCacheMiss(
LOG.debug("{} request marked only-if-cached", exchangeId);
}
final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
cacheStatus(context).suppress();
final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
triggerResponse(cacheResponse, scope, asyncExecCallback);
return;
Expand All @@ -1370,6 +1397,7 @@ public void completed(final Collection<CacheHit> variants) {
if (variants != null && !variants.isEmpty()) {
negotiateResponseFromVariants(requestCacheControl, target, request, scope, chain, asyncExecCallback, variants);
} else {
cacheStatus(HttpCacheContext.cast(scope.clientContext)).forward(CacheStatus.ForwardReason.VARY_MISS);
callBackend(requestCacheControl, target, request, scope, chain, asyncExecCallback);
}
}
Expand All @@ -1386,6 +1414,9 @@ public void cancelled() {

}));
} else {
cacheStatus(HttpCacheContext.cast(scope.clientContext)).forward(partialMatch != null
? CacheStatus.ForwardReason.VARY_MISS
: CacheStatus.ForwardReason.URI_MISS);
callBackend(requestCacheControl, target, request, scope, chain, asyncExecCallback);
}
}
Expand All @@ -1400,6 +1431,7 @@ void negotiateResponseFromVariants(
final Collection<CacheHit> variants) {
final String exchangeId = scope.exchangeId;
final CancellableDependency operation = scope.cancellableDependency;
cacheStatus(HttpCacheContext.cast(scope.clientContext)).forward(CacheStatus.ForwardReason.VARY_MISS);
final Map<ETag, CacheHit> variantMap = new HashMap<>();
for (final CacheHit variant : variants) {
final ETag eTag = variant.entry.getETag();
Expand All @@ -1419,8 +1451,8 @@ void negotiateResponseFromVariants(

void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final CacheHit match) {
final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
cacheUpdates.getAndIncrement();
cacheStatus(context).forwardStatus(HttpStatus.SC_NOT_MODIFIED);

operation.setDependency(responseCache.storeFromNegotiated(
match,
Expand Down
Loading
Loading