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:
Arturo Bernal 2023-03-11 20:39:07 +01:00 committed by Oleg Kalnichevski
parent b915a3ab33
commit 7bf84b71d4
9 changed files with 272 additions and 30 deletions

View File

@ -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);

View File

@ -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.
* <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) {
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();
}

View File

@ -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 +
'}';
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -67,7 +67,6 @@ public class CachingExecBase {
final AtomicLong cacheUpdates = new AtomicLong();
final Map<ProtocolVersion, String> 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 {
}
}
}
}

View File

@ -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<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
* 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 {

View File

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

View File

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