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()); + } + }