diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
index d4dc04cf0..d22432612 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java
@@ -650,7 +650,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
LOG.debug("Revalidating cache entry");
if (cacheRevalidator != null
&& !staleResponseNotAllowed(request, entry, now)
- && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
+ && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
+ || responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
LOG.debug("Serving stale with asynchronous revalidation");
try {
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
index 2f2ece6be..87ed4dbf4 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
@@ -154,6 +154,8 @@ public class CacheConfig implements Cloneable {
private final boolean freshnessCheckEnabled;
private final int asynchronousWorkers;
private final boolean neverCacheHTTP10ResponsesWithQuery;
+ private final boolean staleIfErrorEnabled;
+
/**
* 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 int asynchronousWorkers,
final boolean neverCacheHTTP10ResponsesWithQuery,
- final boolean neverCacheHTTP11ResponsesWithQuery) {
+ final boolean neverCacheHTTP11ResponsesWithQuery,
+ final boolean staleIfErrorEnabled) {
super();
this.maxObjectSize = maxObjectSize;
this.maxCacheEntries = maxCacheEntries;
@@ -189,6 +192,7 @@ public class CacheConfig implements Cloneable {
this.asynchronousWorkers = asynchronousWorkers;
this.neverCacheHTTP10ResponsesWithQuery = neverCacheHTTP10ResponsesWithQuery;
this.neverCacheHTTP11ResponsesWithQuery = neverCacheHTTP11ResponsesWithQuery;
+ this.staleIfErrorEnabled = staleIfErrorEnabled;
}
/**
@@ -226,6 +230,19 @@ public class CacheConfig implements Cloneable {
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.
*/
@@ -328,7 +345,8 @@ public class CacheConfig implements Cloneable {
.setSharedCache(config.isSharedCache())
.setAsynchronousWorkers(config.getAsynchronousWorkers())
.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 boolean neverCacheHTTP10ResponsesWithQuery;
private boolean neverCacheHTTP11ResponsesWithQuery;
+ private boolean staleIfErrorEnabled;
Builder() {
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
@@ -360,6 +379,7 @@ public class CacheConfig implements Cloneable {
this.sharedCache = true;
this.freshnessCheckEnabled = true;
this.asynchronousWorkers = DEFAULT_ASYNCHRONOUS_WORKERS;
+ this.staleIfErrorEnabled = false;
}
/**
@@ -480,6 +500,24 @@ public class CacheConfig implements Cloneable {
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.
+ *
+ * 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) {
this.freshnessCheckEnabled = freshnessCheckEnabled;
return this;
@@ -511,7 +549,8 @@ public class CacheConfig implements Cloneable {
freshnessCheckEnabled,
asynchronousWorkers,
neverCacheHTTP10ResponsesWithQuery,
- neverCacheHTTP11ResponsesWithQuery);
+ neverCacheHTTP11ResponsesWithQuery,
+ staleIfErrorEnabled);
}
}
@@ -532,6 +571,7 @@ public class CacheConfig implements Cloneable {
.append(", asynchronousWorkers=").append(this.asynchronousWorkers)
.append(", neverCacheHTTP10ResponsesWithQuery=").append(this.neverCacheHTTP10ResponsesWithQuery)
.append(", neverCacheHTTP11ResponsesWithQuery=").append(this.neverCacheHTTP11ResponsesWithQuery)
+ .append(", staleIfErrorEnabled=").append(this.staleIfErrorEnabled)
.append("]");
return builder.toString();
}
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
index 7c66c06ec..276268097 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
@@ -88,15 +88,20 @@ final class CacheControl {
* Indicates whether the Cache-Control header includes the "public" directive.
*/
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.
* 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() {
- 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.
*/
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.sharedMaxAge = sharedMaxAge;
this.noCache = noCache;
@@ -122,6 +128,7 @@ final class CacheControl {
this.mustRevalidate = mustRevalidate;
this.proxyRevalidate = proxyRevalidate;
this.cachePublic = cachePublic;
+ this.stale_while_revalidate = stale_while_revalidate;
}
@@ -198,6 +205,15 @@ final class CacheControl {
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,
* no-store, private, must-revalidate, proxy-revalidate, and public values.
@@ -209,12 +225,13 @@ final class CacheControl {
return "CacheControl{" +
"maxAge=" + maxAge +
", sharedMaxAge=" + sharedMaxAge +
- ", isNoCache=" + noCache +
- ", isNoStore=" + noStore +
- ", isPrivate=" + cachePrivate +
+ ", noCache=" + noCache +
+ ", noStore=" + noStore +
+ ", cachePrivate=" + cachePrivate +
", mustRevalidate=" + mustRevalidate +
", proxyRevalidate=" + proxyRevalidate +
- ", isPublic=" + cachePublic +
+ ", cachePublic=" + cachePublic +
+ ", stale_while_revalidate=" + stale_while_revalidate +
'}';
}
}
\ No newline at end of file
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
index 96e49f1d2..af439698b 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
@@ -73,7 +73,6 @@ class CacheControlHeaderParser {
private final static char EQUAL_CHAR = '=';
- private final static char SEMICOLON_CHAR = ';';
/**
* The set of characters that can delimit a token in the header.
@@ -140,6 +139,7 @@ class CacheControlHeaderParser {
boolean mustRevalidate = false;
boolean proxyRevalidate = false;
boolean cachePublic = false;
+ long staleWhileRevalidate = -1;
while (!cursor.atEnd()) {
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
@@ -170,9 +170,11 @@ class CacheControlHeaderParser {
proxyRevalidate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) {
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) {
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
index 5b437eec3..7d7d392aa 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java
@@ -274,7 +274,8 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
try {
if (cacheRevalidator != null
&& !staleResponseNotAllowed(request, entry, now)
- && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
+ && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
+ || responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
LOG.debug("Serving stale with asynchronous revalidation");
final String exchangeId = ExecSupport.getNextExchangeId();
context.setExchangeId(exchangeId);
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
index b947846c7..303cc82a1 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
@@ -67,7 +67,6 @@ public class CachingExecBase {
final AtomicLong cacheUpdates = new AtomicLong();
final Map viaHeaders = new ConcurrentHashMap<>(4);
-
final ResponseCachingPolicy responseCachingPolicy;
final CacheValidityPolicy validityPolicy;
final CachedHttpResponseGenerator responseGenerator;
@@ -112,7 +111,8 @@ public class CachingExecBase {
this.cacheConfig.isSharedCache(),
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
this.cacheConfig.is303CachingEnabled(),
- this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery());
+ this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery(),
+ this.cacheConfig.isStaleIfErrorEnabled());
}
/**
@@ -369,5 +369,4 @@ public class CachingExecBase {
}
}
}
-
}
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java
index f8410789b..bcd82697c 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java
@@ -36,6 +36,7 @@ import java.util.Iterator;
import java.util.Set;
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.core5.http.Header;
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.HttpStatus;
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.message.MessageSupport;
+import org.apache.hc.core5.util.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,6 +89,14 @@ class ResponseCachingPolicy {
private final boolean neverCache1_1ResponsesWithQueryString;
private final Set 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
* in the cache to a maximum of {@link HttpResponse} bytes in size.
@@ -104,6 +115,38 @@ class ResponseCachingPolicy {
final boolean neverCache1_0ResponsesWithQueryString,
final boolean allow303Caching,
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.sharedCache = sharedCache;
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
@@ -113,6 +156,7 @@ class ResponseCachingPolicy {
} else {
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
* @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;
if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)
@@ -193,7 +237,6 @@ class ResponseCachingPolicy {
return false;
}
}
- final CacheControl cacheControl = parseCacheControlHeader(response);
if (isExplicitlyNonCacheable(cacheControl)) {
LOG.debug("Response is explicitly non-cacheable");
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
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
* cacheable response.
@@ -272,7 +345,7 @@ class ResponseCachingPolicy {
* @param response the {@link HttpResponse} from the origin
* @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;
if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) {
if (LOG.isDebugEnabled()) {
@@ -280,7 +353,6 @@ class ResponseCachingPolicy {
}
return false;
}
- final CacheControl cacheControl = parseCacheControlHeader(response);
if (cacheControl != null && cacheControl.isNoStore()) {
LOG.debug("Response is explicitly non-cacheable per cache control directive");
return false;
@@ -310,7 +382,7 @@ class ResponseCachingPolicy {
}
final String method = request.getMethod();
- return isResponseCacheable(method, response);
+ return isResponseCacheable(method, response, 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.
*
- * @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
*/
- private CacheControl parseCacheControlHeader(final HttpResponse response) {
- final Header cacheControlHeader = response.getFirstHeader(HttpHeaders.CACHE_CONTROL);
+ private CacheControl parseCacheControlHeader(final MessageHeaders messageHeaders) {
+ final Header cacheControlHeader = messageHeaders.getFirstHeader(HttpHeaders.CACHE_CONTROL);
if (cacheControlHeader == null) {
return null;
} else {
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java
index eb4668fac..df25500be 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java
@@ -159,4 +159,11 @@ public class CacheControlParserTest {
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());
+ }
}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java
index 75a7ca79e..f701e29a8 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java
@@ -28,11 +28,13 @@ package org.apache.hc.client5.http.impl.cache;
import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
+import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -79,9 +81,9 @@ public class TestCachingExecChain {
ExecRuntime mockExecRuntime;
@Mock
HttpCacheStorage mockStorage;
+ private DefaultCacheRevalidator cacheRevalidator;
@Spy
HttpCache cache = new BasicHttpCache();
-
CacheConfig config;
HttpRoute route;
HttpHost host;
@@ -94,12 +96,12 @@ public class TestCachingExecChain {
public void setUp() {
MockitoAnnotations.openMocks(this);
config = CacheConfig.DEFAULT;
-
host = new HttpHost("foo.example.com", 80);
route = new HttpRoute(host);
request = new BasicClassicHttpRequest("GET", "/stuff");
context = HttpCacheContext.create();
entry = HttpTestUtils.makeCacheEntry();
+ cacheRevalidator = mock(DefaultCacheRevalidator.class);
impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
}
@@ -1017,7 +1019,7 @@ public class TestCachingExecChain {
@Test
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);
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());
}
+ @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());
+ }
+
}