Extend stale-if-error to apply to non-revalidatable cache entries.
The stale-if-error Cache-Control directive is used to indicate that a cached response can be used to satisfy a request even when an error occurs, as long as the response is still fresh or within the specified staleness limit. However, in the current implementation, this directive is only applied to cache entries that are revalidatable, meaning they have an ETag or Last-Modified header and can be refreshed with a conditional request. This commit extends the stale-if-error directive to apply to any stale cache entry, whether revalidatable or not. This ensures that clients will continue to receive a cached response even if the original request resulted in an error, and helps to reduce the load on origin servers.
This commit is contained in:
parent
b915a3ab33
commit
7bf84b71d4
|
@ -650,7 +650,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
||||||
LOG.debug("Revalidating cache entry");
|
LOG.debug("Revalidating cache entry");
|
||||||
if (cacheRevalidator != null
|
if (cacheRevalidator != null
|
||||||
&& !staleResponseNotAllowed(request, entry, now)
|
&& !staleResponseNotAllowed(request, entry, now)
|
||||||
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
|
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
|
||||||
|
|| responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
|
||||||
LOG.debug("Serving stale with asynchronous revalidation");
|
LOG.debug("Serving stale with asynchronous revalidation");
|
||||||
try {
|
try {
|
||||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
|
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
|
||||||
|
|
|
@ -154,6 +154,8 @@ public class CacheConfig implements Cloneable {
|
||||||
private final boolean freshnessCheckEnabled;
|
private final boolean freshnessCheckEnabled;
|
||||||
private final int asynchronousWorkers;
|
private final int asynchronousWorkers;
|
||||||
private final boolean neverCacheHTTP10ResponsesWithQuery;
|
private final boolean neverCacheHTTP10ResponsesWithQuery;
|
||||||
|
private final boolean staleIfErrorEnabled;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A constant indicating whether HTTP/1.1 responses with a query string should never be cached.
|
* A constant indicating whether HTTP/1.1 responses with a query string should never be cached.
|
||||||
|
@ -174,7 +176,8 @@ public class CacheConfig implements Cloneable {
|
||||||
final boolean freshnessCheckEnabled,
|
final boolean freshnessCheckEnabled,
|
||||||
final int asynchronousWorkers,
|
final int asynchronousWorkers,
|
||||||
final boolean neverCacheHTTP10ResponsesWithQuery,
|
final boolean neverCacheHTTP10ResponsesWithQuery,
|
||||||
final boolean neverCacheHTTP11ResponsesWithQuery) {
|
final boolean neverCacheHTTP11ResponsesWithQuery,
|
||||||
|
final boolean staleIfErrorEnabled) {
|
||||||
super();
|
super();
|
||||||
this.maxObjectSize = maxObjectSize;
|
this.maxObjectSize = maxObjectSize;
|
||||||
this.maxCacheEntries = maxCacheEntries;
|
this.maxCacheEntries = maxCacheEntries;
|
||||||
|
@ -189,6 +192,7 @@ public class CacheConfig implements Cloneable {
|
||||||
this.asynchronousWorkers = asynchronousWorkers;
|
this.asynchronousWorkers = asynchronousWorkers;
|
||||||
this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
|
this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
|
||||||
this.neverCacheHTTP11ResponsesWithQuery = neverCacheHTTP11ResponsesWithQuery;
|
this.neverCacheHTTP11ResponsesWithQuery = neverCacheHTTP11ResponsesWithQuery;
|
||||||
|
this.staleIfErrorEnabled = staleIfErrorEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,6 +230,19 @@ public class CacheConfig implements Cloneable {
|
||||||
return neverCacheHTTP11ResponsesWithQuery;
|
return neverCacheHTTP11ResponsesWithQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean value indicating whether the stale-if-error cache
|
||||||
|
* directive is enabled. If this option is enabled, cached responses that
|
||||||
|
* have become stale due to an error (such as a server error or a network
|
||||||
|
* failure) will be returned instead of generating a new request. This can
|
||||||
|
* help to reduce the load on the origin server and improve performance.
|
||||||
|
* @return {@code true} if the stale-if-error directive is enabled, or
|
||||||
|
* {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isStaleIfErrorEnabled() {
|
||||||
|
return this.staleIfErrorEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the maximum number of cache entries the cache will retain.
|
* Returns the maximum number of cache entries the cache will retain.
|
||||||
*/
|
*/
|
||||||
|
@ -328,7 +345,8 @@ public class CacheConfig implements Cloneable {
|
||||||
.setSharedCache(config.isSharedCache())
|
.setSharedCache(config.isSharedCache())
|
||||||
.setAsynchronousWorkers(config.getAsynchronousWorkers())
|
.setAsynchronousWorkers(config.getAsynchronousWorkers())
|
||||||
.setNeverCacheHTTP10ResponsesWithQueryString(config.isNeverCacheHTTP10ResponsesWithQuery())
|
.setNeverCacheHTTP10ResponsesWithQueryString(config.isNeverCacheHTTP10ResponsesWithQuery())
|
||||||
.setNeverCacheHTTP11ResponsesWithQueryString(config.isNeverCacheHTTP11ResponsesWithQuery());
|
.setNeverCacheHTTP11ResponsesWithQueryString(config.isNeverCacheHTTP11ResponsesWithQuery())
|
||||||
|
.setStaleIfErrorEnabled(config.isStaleIfErrorEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -347,6 +365,7 @@ public class CacheConfig implements Cloneable {
|
||||||
private int asynchronousWorkers;
|
private int asynchronousWorkers;
|
||||||
private boolean neverCacheHTTP10ResponsesWithQuery;
|
private boolean neverCacheHTTP10ResponsesWithQuery;
|
||||||
private boolean neverCacheHTTP11ResponsesWithQuery;
|
private boolean neverCacheHTTP11ResponsesWithQuery;
|
||||||
|
private boolean staleIfErrorEnabled;
|
||||||
|
|
||||||
Builder() {
|
Builder() {
|
||||||
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
|
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
|
||||||
|
@ -360,6 +379,7 @@ public class CacheConfig implements Cloneable {
|
||||||
this.sharedCache = true;
|
this.sharedCache = true;
|
||||||
this.freshnessCheckEnabled = true;
|
this.freshnessCheckEnabled = true;
|
||||||
this.asynchronousWorkers = DEFAULT_ASYNCHRONOUS_WORKERS;
|
this.asynchronousWorkers = DEFAULT_ASYNCHRONOUS_WORKERS;
|
||||||
|
this.staleIfErrorEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -480,6 +500,24 @@ public class CacheConfig implements Cloneable {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the stale-if-error cache directive. If this option
|
||||||
|
* is enabled, cached responses that have become stale due to an error (such
|
||||||
|
* as a server error or a network failure) will be returned instead of
|
||||||
|
* generating a new request. This can help to reduce the load on the origin
|
||||||
|
* server and improve performance.
|
||||||
|
* <p>
|
||||||
|
* By default, the stale-if-error directive is disabled.
|
||||||
|
*
|
||||||
|
* @param enabled a boolean value indicating whether the stale-if-error
|
||||||
|
* directive should be enabled.
|
||||||
|
* @return the builder object
|
||||||
|
*/
|
||||||
|
public Builder setStaleIfErrorEnabled(final boolean enabled) {
|
||||||
|
this.staleIfErrorEnabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder setFreshnessCheckEnabled(final boolean freshnessCheckEnabled) {
|
public Builder setFreshnessCheckEnabled(final boolean freshnessCheckEnabled) {
|
||||||
this.freshnessCheckEnabled = freshnessCheckEnabled;
|
this.freshnessCheckEnabled = freshnessCheckEnabled;
|
||||||
return this;
|
return this;
|
||||||
|
@ -511,7 +549,8 @@ public class CacheConfig implements Cloneable {
|
||||||
freshnessCheckEnabled,
|
freshnessCheckEnabled,
|
||||||
asynchronousWorkers,
|
asynchronousWorkers,
|
||||||
neverCacheHTTP10ResponsesWithQuery,
|
neverCacheHTTP10ResponsesWithQuery,
|
||||||
neverCacheHTTP11ResponsesWithQuery);
|
neverCacheHTTP11ResponsesWithQuery,
|
||||||
|
staleIfErrorEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -532,6 +571,7 @@ public class CacheConfig implements Cloneable {
|
||||||
.append(", asynchronousWorkers=").append(this.asynchronousWorkers)
|
.append(", asynchronousWorkers=").append(this.asynchronousWorkers)
|
||||||
.append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
|
.append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
|
||||||
.append(", neverCacheHTTP11ResponsesWithQuery=").append(this.neverCacheHTTP11ResponsesWithQuery)
|
.append(", neverCacheHTTP11ResponsesWithQuery=").append(this.neverCacheHTTP11ResponsesWithQuery)
|
||||||
|
.append(", staleIfErrorEnabled=").append(this.staleIfErrorEnabled)
|
||||||
.append("]");
|
.append("]");
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,15 +88,20 @@ final class CacheControl {
|
||||||
* Indicates whether the Cache-Control header includes the "public" directive.
|
* Indicates whether the Cache-Control header includes the "public" directive.
|
||||||
*/
|
*/
|
||||||
private final boolean cachePublic;
|
private final boolean cachePublic;
|
||||||
|
/**
|
||||||
|
* The number of seconds that a stale response is considered fresh for the purpose
|
||||||
|
* of serving a response while a revalidation request is made to the origin server.
|
||||||
|
*/
|
||||||
|
private final long stale_while_revalidate;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of {@code CacheControl} with default values.
|
* Creates a new instance of {@code CacheControl} with default values.
|
||||||
* The default values are: max-age=-1, shared-max-age=-1, must-revalidate=false, no-cache=false,
|
* The default values are: max-age=-1, shared-max-age=-1, must-revalidate=false, no-cache=false,
|
||||||
* no-store=false, private=false, proxy-revalidate=false, and public=false.
|
* no-store=false, private=false, proxy-revalidate=false, public=false and stale_while_revalidate=-1.
|
||||||
*/
|
*/
|
||||||
public CacheControl() {
|
public CacheControl() {
|
||||||
this(-1, -1, false, false, false, false, false, false);
|
this(-1, -1, false, false, false, false, false, false,-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,7 +118,8 @@ final class CacheControl {
|
||||||
* @param cachePublic The public value from the Cache-Control header.
|
* @param cachePublic The public value from the Cache-Control header.
|
||||||
*/
|
*/
|
||||||
public CacheControl(final long maxAge, final long sharedMaxAge, final boolean mustRevalidate, final boolean noCache, final boolean noStore,
|
public CacheControl(final long maxAge, final long sharedMaxAge, final boolean mustRevalidate, final boolean noCache, final boolean noStore,
|
||||||
final boolean cachePrivate, final boolean proxyRevalidate, final boolean cachePublic) {
|
final boolean cachePrivate, final boolean proxyRevalidate, final boolean cachePublic,
|
||||||
|
final long stale_while_revalidate) {
|
||||||
this.maxAge = maxAge;
|
this.maxAge = maxAge;
|
||||||
this.sharedMaxAge = sharedMaxAge;
|
this.sharedMaxAge = sharedMaxAge;
|
||||||
this.noCache = noCache;
|
this.noCache = noCache;
|
||||||
|
@ -122,6 +128,7 @@ final class CacheControl {
|
||||||
this.mustRevalidate = mustRevalidate;
|
this.mustRevalidate = mustRevalidate;
|
||||||
this.proxyRevalidate = proxyRevalidate;
|
this.proxyRevalidate = proxyRevalidate;
|
||||||
this.cachePublic = cachePublic;
|
this.cachePublic = cachePublic;
|
||||||
|
this.stale_while_revalidate = stale_while_revalidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,6 +205,15 @@ final class CacheControl {
|
||||||
return cachePublic;
|
return cachePublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stale-while-revalidate value from the Cache-Control header.
|
||||||
|
*
|
||||||
|
* @return The stale-while-revalidate value.
|
||||||
|
*/
|
||||||
|
public long getStaleWhileRevalidate() {
|
||||||
|
return stale_while_revalidate;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string representation of the {@code CacheControl} object, including the max-age, shared-max-age, no-cache,
|
* Returns a string representation of the {@code CacheControl} object, including the max-age, shared-max-age, no-cache,
|
||||||
* no-store, private, must-revalidate, proxy-revalidate, and public values.
|
* no-store, private, must-revalidate, proxy-revalidate, and public values.
|
||||||
|
@ -209,12 +225,13 @@ final class CacheControl {
|
||||||
return "CacheControl{" +
|
return "CacheControl{" +
|
||||||
"maxAge=" + maxAge +
|
"maxAge=" + maxAge +
|
||||||
", sharedMaxAge=" + sharedMaxAge +
|
", sharedMaxAge=" + sharedMaxAge +
|
||||||
", isNoCache=" + noCache +
|
", noCache=" + noCache +
|
||||||
", isNoStore=" + noStore +
|
", noStore=" + noStore +
|
||||||
", isPrivate=" + cachePrivate +
|
", cachePrivate=" + cachePrivate +
|
||||||
", mustRevalidate=" + mustRevalidate +
|
", mustRevalidate=" + mustRevalidate +
|
||||||
", proxyRevalidate=" + proxyRevalidate +
|
", proxyRevalidate=" + proxyRevalidate +
|
||||||
", isPublic=" + cachePublic +
|
", cachePublic=" + cachePublic +
|
||||||
|
", stale_while_revalidate=" + stale_while_revalidate +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -73,7 +73,6 @@ class CacheControlHeaderParser {
|
||||||
|
|
||||||
|
|
||||||
private final static char EQUAL_CHAR = '=';
|
private final static char EQUAL_CHAR = '=';
|
||||||
private final static char SEMICOLON_CHAR = ';';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set of characters that can delimit a token in the header.
|
* The set of characters that can delimit a token in the header.
|
||||||
|
@ -140,6 +139,7 @@ class CacheControlHeaderParser {
|
||||||
boolean mustRevalidate = false;
|
boolean mustRevalidate = false;
|
||||||
boolean proxyRevalidate = false;
|
boolean proxyRevalidate = false;
|
||||||
boolean cachePublic = false;
|
boolean cachePublic = false;
|
||||||
|
long staleWhileRevalidate = -1;
|
||||||
|
|
||||||
while (!cursor.atEnd()) {
|
while (!cursor.atEnd()) {
|
||||||
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
|
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
|
||||||
|
@ -170,9 +170,11 @@ class CacheControlHeaderParser {
|
||||||
proxyRevalidate = true;
|
proxyRevalidate = true;
|
||||||
} else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) {
|
} else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) {
|
||||||
cachePublic = true;
|
cachePublic = true;
|
||||||
|
} else if (name.equalsIgnoreCase(HeaderConstants.STALE_WHILE_REVALIDATE)) {
|
||||||
|
staleWhileRevalidate = parseSeconds(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new CacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate, cachePublic);
|
return new CacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate, cachePublic, staleWhileRevalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long parseSeconds(final String name, final String value) {
|
private static long parseSeconds(final String name, final String value) {
|
||||||
|
|
|
@ -274,7 +274,8 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
||||||
try {
|
try {
|
||||||
if (cacheRevalidator != null
|
if (cacheRevalidator != null
|
||||||
&& !staleResponseNotAllowed(request, entry, now)
|
&& !staleResponseNotAllowed(request, entry, now)
|
||||||
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
|
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
|
||||||
|
|| responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
|
||||||
LOG.debug("Serving stale with asynchronous revalidation");
|
LOG.debug("Serving stale with asynchronous revalidation");
|
||||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||||
context.setExchangeId(exchangeId);
|
context.setExchangeId(exchangeId);
|
||||||
|
|
|
@ -67,7 +67,6 @@ public class CachingExecBase {
|
||||||
final AtomicLong cacheUpdates = new AtomicLong();
|
final AtomicLong cacheUpdates = new AtomicLong();
|
||||||
|
|
||||||
final Map<ProtocolVersion, String> viaHeaders = new ConcurrentHashMap<>(4);
|
final Map<ProtocolVersion, String> viaHeaders = new ConcurrentHashMap<>(4);
|
||||||
|
|
||||||
final ResponseCachingPolicy responseCachingPolicy;
|
final ResponseCachingPolicy responseCachingPolicy;
|
||||||
final CacheValidityPolicy validityPolicy;
|
final CacheValidityPolicy validityPolicy;
|
||||||
final CachedHttpResponseGenerator responseGenerator;
|
final CachedHttpResponseGenerator responseGenerator;
|
||||||
|
@ -112,7 +111,8 @@ public class CachingExecBase {
|
||||||
this.cacheConfig.isSharedCache(),
|
this.cacheConfig.isSharedCache(),
|
||||||
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
|
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
|
||||||
this.cacheConfig.is303CachingEnabled(),
|
this.cacheConfig.is303CachingEnabled(),
|
||||||
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery());
|
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery(),
|
||||||
|
this.cacheConfig.isStaleIfErrorEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -369,5 +369,4 @@ public class CachingExecBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import java.util.Iterator;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.hc.client5.http.cache.HeaderConstants;
|
import org.apache.hc.client5.http.cache.HeaderConstants;
|
||||||
|
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||||
import org.apache.hc.client5.http.utils.DateUtils;
|
import org.apache.hc.client5.http.utils.DateUtils;
|
||||||
import org.apache.hc.core5.http.Header;
|
import org.apache.hc.core5.http.Header;
|
||||||
import org.apache.hc.core5.http.HeaderElement;
|
import org.apache.hc.core5.http.HeaderElement;
|
||||||
|
@ -45,8 +46,10 @@ import org.apache.hc.core5.http.HttpRequest;
|
||||||
import org.apache.hc.core5.http.HttpResponse;
|
import org.apache.hc.core5.http.HttpResponse;
|
||||||
import org.apache.hc.core5.http.HttpStatus;
|
import org.apache.hc.core5.http.HttpStatus;
|
||||||
import org.apache.hc.core5.http.HttpVersion;
|
import org.apache.hc.core5.http.HttpVersion;
|
||||||
|
import org.apache.hc.core5.http.MessageHeaders;
|
||||||
import org.apache.hc.core5.http.ProtocolVersion;
|
import org.apache.hc.core5.http.ProtocolVersion;
|
||||||
import org.apache.hc.core5.http.message.MessageSupport;
|
import org.apache.hc.core5.http.message.MessageSupport;
|
||||||
|
import org.apache.hc.core5.util.Args;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -86,6 +89,14 @@ class ResponseCachingPolicy {
|
||||||
private final boolean neverCache1_1ResponsesWithQueryString;
|
private final boolean neverCache1_1ResponsesWithQueryString;
|
||||||
private final Set<Integer> uncacheableStatusCodes;
|
private final Set<Integer> uncacheableStatusCodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a cache policy that limits the size of things that should be stored
|
* Define a cache policy that limits the size of things that should be stored
|
||||||
* in the cache to a maximum of {@link HttpResponse} bytes in size.
|
* in the cache to a maximum of {@link HttpResponse} bytes in size.
|
||||||
|
@ -104,6 +115,38 @@ class ResponseCachingPolicy {
|
||||||
final boolean neverCache1_0ResponsesWithQueryString,
|
final boolean neverCache1_0ResponsesWithQueryString,
|
||||||
final boolean allow303Caching,
|
final boolean allow303Caching,
|
||||||
final boolean neverCache1_1ResponsesWithQueryString) {
|
final boolean neverCache1_1ResponsesWithQueryString) {
|
||||||
|
this(maxObjectSizeBytes,
|
||||||
|
sharedCache,
|
||||||
|
neverCache1_0ResponsesWithQueryString,
|
||||||
|
allow303Caching,
|
||||||
|
neverCache1_1ResponsesWithQueryString,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new ResponseCachingPolicy with the specified cache policy settings and stale-if-error support.
|
||||||
|
*
|
||||||
|
* @param maxObjectSizeBytes the maximum size of objects, in bytes, that should be stored
|
||||||
|
* in the cache
|
||||||
|
* @param sharedCache whether to behave as a shared cache (true) or a
|
||||||
|
* non-shared/private cache (false)
|
||||||
|
* @param neverCache1_0ResponsesWithQueryString {@code true} to never cache HTTP 1.0 responses with a query string,
|
||||||
|
* {@code false} to cache if explicit cache headers are found.
|
||||||
|
* @param allow303Caching {@code true} if this policy is permitted to cache 303 responses,
|
||||||
|
* {@code false} otherwise
|
||||||
|
* @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 long maxObjectSizeBytes,
|
||||||
|
final boolean sharedCache,
|
||||||
|
final boolean neverCache1_0ResponsesWithQueryString,
|
||||||
|
final boolean allow303Caching,
|
||||||
|
final boolean neverCache1_1ResponsesWithQueryString,
|
||||||
|
final boolean staleIfErrorEnabled) {
|
||||||
this.maxObjectSizeBytes = maxObjectSizeBytes;
|
this.maxObjectSizeBytes = maxObjectSizeBytes;
|
||||||
this.sharedCache = sharedCache;
|
this.sharedCache = sharedCache;
|
||||||
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
|
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
|
||||||
|
@ -113,6 +156,7 @@ class ResponseCachingPolicy {
|
||||||
} else {
|
} else {
|
||||||
uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
|
uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
|
||||||
}
|
}
|
||||||
|
this.staleIfErrorEnabled = staleIfErrorEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,7 +166,7 @@ class ResponseCachingPolicy {
|
||||||
* @param response The origin response
|
* @param response The origin response
|
||||||
* @return {@code true} if response is cacheable
|
* @return {@code true} if response is cacheable
|
||||||
*/
|
*/
|
||||||
public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
|
public boolean isResponseCacheable(final String httpMethod, final HttpResponse response, final CacheControl cacheControl) {
|
||||||
boolean cacheable = false;
|
boolean cacheable = false;
|
||||||
|
|
||||||
if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)
|
if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)
|
||||||
|
@ -193,7 +237,6 @@ class ResponseCachingPolicy {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final CacheControl cacheControl = parseCacheControlHeader(response);
|
|
||||||
if (isExplicitlyNonCacheable(cacheControl)) {
|
if (isExplicitlyNonCacheable(cacheControl)) {
|
||||||
LOG.debug("Response is explicitly non-cacheable");
|
LOG.debug("Response is explicitly non-cacheable");
|
||||||
return false;
|
return false;
|
||||||
|
@ -236,7 +279,7 @@ class ResponseCachingPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated As of version 5.0, use {@link ResponseCachingPolicy#parseCacheControlHeader(HttpResponse)} instead.
|
* @deprecated As of version 5.0, use {@link ResponseCachingPolicy#parseCacheControlHeader(MessageHeaders)} instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
|
protected boolean hasCacheControlParameterFrom(final HttpMessage msg, final String[] params) {
|
||||||
|
@ -264,6 +307,36 @@ class ResponseCachingPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the {@link HttpResponse} gotten from the origin is a
|
||||||
|
* cacheable response.
|
||||||
|
*
|
||||||
|
* @param request the {@link HttpRequest} that generated an origin hit. Can't be {@code null}.
|
||||||
|
* @param response the {@link HttpResponse} from the origin. Can't be {@code null}.
|
||||||
|
* @return {@code true} if response is cacheable
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
|
||||||
|
Args.notNull(request, "Request");
|
||||||
|
Args.notNull(response, "Response");
|
||||||
|
return isResponseCacheable(request, response, parseCacheControlHeader(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an HttpResponse can be cached.
|
||||||
|
*
|
||||||
|
* @param httpMethod What type of request was this, a GET, PUT, other?. Can't be {@code null}.
|
||||||
|
* @param response The origin response. Can't be {@code null}.
|
||||||
|
* @return {@code true} if response is cacheable
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
|
||||||
|
Args.notEmpty(httpMethod, "httpMethod");
|
||||||
|
Args.notNull(response, "Response");
|
||||||
|
return isResponseCacheable(httpMethod, response, parseCacheControlHeader(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the {@link HttpResponse} gotten from the origin is a
|
* Determine if the {@link HttpResponse} gotten from the origin is a
|
||||||
* cacheable response.
|
* cacheable response.
|
||||||
|
@ -272,7 +345,7 @@ class ResponseCachingPolicy {
|
||||||
* @param response the {@link HttpResponse} from the origin
|
* @param response the {@link HttpResponse} from the origin
|
||||||
* @return {@code true} if response is cacheable
|
* @return {@code true} if response is cacheable
|
||||||
*/
|
*/
|
||||||
public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response) {
|
public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response, final CacheControl cacheControl) {
|
||||||
final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.DEFAULT;
|
final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.DEFAULT;
|
||||||
if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) {
|
if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) {
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
|
@ -280,7 +353,6 @@ class ResponseCachingPolicy {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final CacheControl cacheControl = parseCacheControlHeader(response);
|
|
||||||
if (cacheControl != null && cacheControl.isNoStore()) {
|
if (cacheControl != null && cacheControl.isNoStore()) {
|
||||||
LOG.debug("Response is explicitly non-cacheable per cache control directive");
|
LOG.debug("Response is explicitly non-cacheable per cache control directive");
|
||||||
return false;
|
return false;
|
||||||
|
@ -310,7 +382,7 @@ class ResponseCachingPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
final String method = request.getMethod();
|
final String method = request.getMethod();
|
||||||
return isResponseCacheable(method, response);
|
return isResponseCacheable(method, response, cacheControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final HttpResponse response, final CacheControl cacheControl) {
|
private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final HttpResponse response, final CacheControl cacheControl) {
|
||||||
|
@ -410,14 +482,42 @@ class ResponseCachingPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the Cache-Control header from the given HTTP response and returns the corresponding CacheControl instance.
|
* 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}.
|
||||||
|
*
|
||||||
|
* @param entry the cached HTTP message entry to check
|
||||||
|
* @return {@code true} if a stale response can be served in case of an error status code, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean isStaleIfErrorEnabled(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
|
||||||
|
final CacheControl cacheControl = parseCacheControlHeader(entry);
|
||||||
|
if (cacheControl == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return cacheControl.getStaleWhileRevalidate() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the Cache-Control header from the given HTTP messageHeaders and returns the corresponding CacheControl instance.
|
||||||
* If the header is not present, returns a CacheControl instance with default values for all directives.
|
* If the header is not present, returns a CacheControl instance with default values for all directives.
|
||||||
*
|
*
|
||||||
* @param response the HTTP response to parse the header from
|
* @param messageHeaders the HTTP message to parse the header from
|
||||||
* @return a CacheControl instance with the parsed directives or default values if the header is not present
|
* @return a CacheControl instance with the parsed directives or default values if the header is not present
|
||||||
*/
|
*/
|
||||||
private CacheControl parseCacheControlHeader(final HttpResponse response) {
|
private CacheControl parseCacheControlHeader(final MessageHeaders messageHeaders) {
|
||||||
final Header cacheControlHeader = response.getFirstHeader(HttpHeaders.CACHE_CONTROL);
|
final Header cacheControlHeader = messageHeaders.getFirstHeader(HttpHeaders.CACHE_CONTROL);
|
||||||
if (cacheControlHeader == null) {
|
if (cacheControlHeader == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -159,4 +159,11 @@ public class CacheControlParserTest {
|
||||||
assertTrue(cacheControl.isNoStore());
|
assertTrue(cacheControl.isNoStore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseStaleWhileRevalidate() {
|
||||||
|
final Header header = new BasicHeader("Cache-Control", "max-age=3600, stale-while-revalidate=120");
|
||||||
|
final CacheControl cacheControl = parser.parse(header);
|
||||||
|
|
||||||
|
assertEquals(120, cacheControl.getStaleWhileRevalidate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,13 @@ package org.apache.hc.client5.http.impl.cache;
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
@ -79,9 +81,9 @@ public class TestCachingExecChain {
|
||||||
ExecRuntime mockExecRuntime;
|
ExecRuntime mockExecRuntime;
|
||||||
@Mock
|
@Mock
|
||||||
HttpCacheStorage mockStorage;
|
HttpCacheStorage mockStorage;
|
||||||
|
private DefaultCacheRevalidator cacheRevalidator;
|
||||||
@Spy
|
@Spy
|
||||||
HttpCache cache = new BasicHttpCache();
|
HttpCache cache = new BasicHttpCache();
|
||||||
|
|
||||||
CacheConfig config;
|
CacheConfig config;
|
||||||
HttpRoute route;
|
HttpRoute route;
|
||||||
HttpHost host;
|
HttpHost host;
|
||||||
|
@ -94,12 +96,12 @@ public class TestCachingExecChain {
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
MockitoAnnotations.openMocks(this);
|
MockitoAnnotations.openMocks(this);
|
||||||
config = CacheConfig.DEFAULT;
|
config = CacheConfig.DEFAULT;
|
||||||
|
|
||||||
host = new HttpHost("foo.example.com", 80);
|
host = new HttpHost("foo.example.com", 80);
|
||||||
route = new HttpRoute(host);
|
route = new HttpRoute(host);
|
||||||
request = new BasicClassicHttpRequest("GET", "/stuff");
|
request = new BasicClassicHttpRequest("GET", "/stuff");
|
||||||
context = HttpCacheContext.create();
|
context = HttpCacheContext.create();
|
||||||
entry = HttpTestUtils.makeCacheEntry();
|
entry = HttpTestUtils.makeCacheEntry();
|
||||||
|
cacheRevalidator = mock(DefaultCacheRevalidator.class);
|
||||||
|
|
||||||
impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
|
impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
|
||||||
}
|
}
|
||||||
|
@ -1017,7 +1019,7 @@ public class TestCachingExecChain {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSmallEnoughResponsesAreCached() throws Exception {
|
public void testSmallEnoughResponsesAreCached() throws Exception {
|
||||||
final HttpCache mockCache = Mockito.mock(HttpCache.class);
|
final HttpCache mockCache = mock(HttpCache.class);
|
||||||
impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
|
impl = new CachingExec(mockCache, null, CacheConfig.DEFAULT);
|
||||||
|
|
||||||
final HttpHost host = new HttpHost("foo.example.com");
|
final HttpHost host = new HttpHost("foo.example.com");
|
||||||
|
@ -1284,4 +1286,77 @@ public class TestCachingExecChain {
|
||||||
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
|
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReturnssetStaleIfErrorNotEnabled() throws Exception {
|
||||||
|
|
||||||
|
// Create the first request and response
|
||||||
|
final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
|
||||||
|
final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/");
|
||||||
|
|
||||||
|
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("Date", DateUtils.formatStandardDate(Instant.now()));
|
||||||
|
resp1.setHeader("Cache-Control", "public");
|
||||||
|
|
||||||
|
req2.addHeader("If-None-Match", "\"abc\"");
|
||||||
|
|
||||||
|
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
|
||||||
|
|
||||||
|
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
|
||||||
|
|
||||||
|
execute(req1);
|
||||||
|
|
||||||
|
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
|
||||||
|
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
|
||||||
|
final ClassicHttpResponse result = execute(req2);
|
||||||
|
Assertions.assertEquals(HttpStatus.SC_OK, result.getCode());
|
||||||
|
|
||||||
|
Mockito.verify(cacheRevalidator, Mockito.never()).revalidateCacheEntry(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReturnssetStaleIfErrorEnabled() throws Exception {
|
||||||
|
final CacheConfig customConfig = CacheConfig.custom()
|
||||||
|
.setMaxCacheEntries(100)
|
||||||
|
.setMaxObjectSize(1024)
|
||||||
|
.setSharedCache(false)
|
||||||
|
.setStaleIfErrorEnabled(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
impl = new CachingExec(cache, cacheRevalidator, customConfig);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
resp1.setEntity(HttpTestUtils.makeBody(128));
|
||||||
|
resp1.setHeader("Content-Length", "128");
|
||||||
|
resp1.setHeader("ETag", "\"etag\"");
|
||||||
|
resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now().minus(Duration.ofHours(10))));
|
||||||
|
resp1.setHeader("Cache-Control", "public, max-age=-1, stale-while-revalidate=1");
|
||||||
|
req2.addHeader("If-None-Match", "\"abc\"");
|
||||||
|
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// Assert that the cache revalidator was called
|
||||||
|
Mockito.verify(cacheRevalidator, Mockito.times(1)).revalidateCacheEntry(Mockito.any(), Mockito.any());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue