Complete the implementation of stale-if-error support as per RFC 5861
- Updated handling of IOExceptions to serve stale responses when allowed by stale-if-error
This commit is contained in:
parent
7bf84b71d4
commit
e1cfb2add6
|
@ -648,10 +648,11 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(entry);
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(request, entry, now)
|
||||
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
|
||||
|| responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
|
||||
|| staleIfErrorEnabled) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
|
||||
|
@ -672,7 +673,22 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
|
|||
asyncExecCallback1 -> revalidateCacheEntry(target, request, entityProducer, fork, chain, asyncExecCallback1, entry));
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex) {
|
||||
asyncExecCallback.failed(ex);
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
try {
|
||||
final SimpleHttpResponse cacheResponse = generateCachedResponse(request, context, entry, now);
|
||||
triggerResponse(cacheResponse, scope, asyncExecCallback);
|
||||
} catch (final ResourceIOException ex2) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Failed to generate cached response, falling back to backend", ex2);
|
||||
}
|
||||
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
|
||||
}
|
||||
} else {
|
||||
asyncExecCallback.failed(ex);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
revalidateCacheEntry(target, request, entityProducer, scope, chain, asyncExecCallback, entry);
|
||||
|
|
|
@ -271,11 +271,12 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
return convert(generateGatewayTimeout(context), scope);
|
||||
} else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
|
||||
LOG.debug("Revalidating cache entry");
|
||||
final boolean staleIfErrorEnabled = responseCachingPolicy.isStaleIfErrorEnabled(entry);
|
||||
try {
|
||||
if (cacheRevalidator != null
|
||||
&& !staleResponseNotAllowed(request, entry, now)
|
||||
&& validityPolicy.mayReturnStaleWhileRevalidating(entry, now)
|
||||
|| responseCachingPolicy.isStaleIfErrorEnabled(entry)) {
|
||||
|| staleIfErrorEnabled) {
|
||||
LOG.debug("Serving stale with asynchronous revalidation");
|
||||
final String exchangeId = ExecSupport.getNextExchangeId();
|
||||
context.setExchangeId(exchangeId);
|
||||
|
@ -293,6 +294,12 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
|
|||
}
|
||||
return revalidateCacheEntry(target, request, scope, chain, entry);
|
||||
} catch (final IOException ioex) {
|
||||
if (staleIfErrorEnabled) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Serving stale response due to IOException and stale-if-error enabled");
|
||||
}
|
||||
return convert(generateCachedResponse(request, context, entry, now), scope);
|
||||
}
|
||||
return convert(handleRevalidationFailure(request, context, entry, now), scope);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -37,6 +37,9 @@ import java.net.SocketTimeoutException;
|
|||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.apache.hc.client5.http.HttpRoute;
|
||||
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
|
||||
|
@ -45,6 +48,7 @@ import org.apache.hc.client5.http.cache.CacheResponseStatus;
|
|||
import org.apache.hc.client5.http.cache.HttpCacheContext;
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.cache.HttpCacheStorage;
|
||||
import org.apache.hc.client5.http.cache.ResourceIOException;
|
||||
import org.apache.hc.client5.http.classic.ExecChain;
|
||||
import org.apache.hc.client5.http.classic.ExecRuntime;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
|
@ -53,6 +57,7 @@ import org.apache.hc.client5.http.protocol.HttpClientContext;
|
|||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||
import org.apache.hc.core5.http.ClassicHttpResponse;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpException;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
|
@ -64,6 +69,7 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
|
|||
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
|
||||
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
|
||||
import org.apache.hc.core5.net.URIAuthority;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -82,6 +88,7 @@ public class TestCachingExecChain {
|
|||
@Mock
|
||||
HttpCacheStorage mockStorage;
|
||||
private DefaultCacheRevalidator cacheRevalidator;
|
||||
private CachedHttpResponseGenerator responseGenerator;
|
||||
@Spy
|
||||
HttpCache cache = new BasicHttpCache();
|
||||
CacheConfig config;
|
||||
|
@ -91,6 +98,15 @@ public class TestCachingExecChain {
|
|||
HttpCacheContext context;
|
||||
HttpCacheEntry entry;
|
||||
CachingExec impl;
|
||||
CacheValidityPolicy validityPolicy;
|
||||
ResponseCachingPolicy responseCachingPolicy;
|
||||
CacheableRequestPolicy cacheableRequestPolicy;
|
||||
CachedResponseSuitabilityChecker suitabilityChecker;
|
||||
ResponseProtocolCompliance responseCompliance;
|
||||
RequestProtocolCompliance requestCompliance;
|
||||
ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
|
||||
CacheConfig customConfig;
|
||||
HttpCache responseCache;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
|
@ -102,6 +118,17 @@ public class TestCachingExecChain {
|
|||
context = HttpCacheContext.create();
|
||||
entry = HttpTestUtils.makeCacheEntry();
|
||||
cacheRevalidator = mock(DefaultCacheRevalidator.class);
|
||||
responseGenerator = mock(CachedHttpResponseGenerator.class);
|
||||
customConfig = mock(CacheConfig.class);
|
||||
|
||||
validityPolicy = mock(CacheValidityPolicy.class);
|
||||
responseCachingPolicy = mock(ResponseCachingPolicy.class);
|
||||
cacheableRequestPolicy = mock(CacheableRequestPolicy.class);
|
||||
suitabilityChecker = mock(CachedResponseSuitabilityChecker.class);
|
||||
responseCompliance = mock(ResponseProtocolCompliance.class);
|
||||
requestCompliance = mock(RequestProtocolCompliance.class);
|
||||
conditionalRequestBuilder = mock(ConditionalRequestBuilder.class);
|
||||
responseCache = mock(HttpCache.class);
|
||||
|
||||
impl = new CachingExec(cache, null, CacheConfig.DEFAULT);
|
||||
}
|
||||
|
@ -1359,4 +1386,63 @@ public class TestCachingExecChain {
|
|||
Mockito.verify(cacheRevalidator, Mockito.times(1)).revalidateCacheEntry(Mockito.any(), Mockito.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaleIfErrorEnabledWithIOException() throws Exception {
|
||||
|
||||
impl = new CachingExec(responseCache, validityPolicy, responseCachingPolicy,
|
||||
responseGenerator, cacheableRequestPolicy, suitabilityChecker,
|
||||
responseCompliance, requestCompliance, cacheRevalidator,
|
||||
conditionalRequestBuilder, 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");
|
||||
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
|
||||
|
||||
// Set up the mock response chain
|
||||
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
|
||||
Mockito.when(responseCachingPolicy.isStaleIfErrorEnabled(Mockito.any())).thenReturn(true);
|
||||
Mockito.when(cacheableRequestPolicy.isServableFromCache(Mockito.any())).thenReturn(true);
|
||||
Mockito.when(validityPolicy.getStaleness(Mockito.any(), Mockito.any())).thenReturn(TimeValue.MAX_VALUE);
|
||||
|
||||
final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK);
|
||||
final AtomicInteger callCount = new AtomicInteger(0);
|
||||
|
||||
Mockito.doAnswer(invocation -> {
|
||||
if (callCount.getAndIncrement() == 0) {
|
||||
throw new ResourceIOException("ResourceIOException");
|
||||
} else {
|
||||
// Replace this with the actual return value for the second call
|
||||
return response;
|
||||
}
|
||||
}).when(responseGenerator).generateResponse(Mockito.any(), Mockito.any());
|
||||
|
||||
// Execute the first request and assert the response
|
||||
final ClassicHttpResponse response1 = execute(req1);
|
||||
final HttpCacheEntry httpCacheEntry = HttpTestUtils.makeCacheEntry();
|
||||
Mockito.when(responseCache.getCacheEntry(Mockito.any(), Mockito.any())).thenReturn(httpCacheEntry);
|
||||
Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, response1.getCode());
|
||||
|
||||
Mockito.when(mockExecRuntime.fork(Mockito.any())).thenReturn(mockExecRuntime);
|
||||
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
|
||||
|
||||
final ClassicHttpResponse response2 = execute(req2);
|
||||
// Verify that the response has the expected status code (e.g., 200)
|
||||
Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
|
||||
|
||||
// Verify that the response has the expected headers or other properties
|
||||
// For example, you can check for the "Warning" header indicating a stale response:
|
||||
final Optional<Header> warningHeader = Arrays.stream(response.getHeaders())
|
||||
.filter(header -> header.getName().equalsIgnoreCase("Warning"))
|
||||
.findFirst();
|
||||
|
||||
Assertions.assertTrue(warningHeader.isPresent());
|
||||
Assertions.assertTrue(warningHeader.get().getValue().contains("110"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue