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 9eb48b49f..2790e9894 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 @@ -40,6 +40,7 @@ import org.apache.hc.client5.http.cache.HeaderConstants; import org.apache.hc.client5.http.cache.HttpCacheContext; import org.apache.hc.client5.http.cache.HttpCacheEntry; import org.apache.hc.client5.http.cache.ResourceIOException; +import org.apache.hc.client5.http.utils.DateUtils; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HeaderElement; import org.apache.hc.core5.http.HttpHeaders; @@ -78,6 +79,8 @@ public class CachingExecBase { private static final Logger LOG = LoggerFactory.getLogger(CachingExecBase.class); + private static final TimeValue ONE_DAY = TimeValue.ofHours(24); + CachingExecBase( final CacheValidityPolicy validityPolicy, final ResponseCachingPolicy responseCachingPolicy, @@ -198,6 +201,23 @@ public class CachingExecBase { if (TimeValue.isPositive(validityPolicy.getStaleness(entry, now))) { cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\""); } + + // Adding Warning: 113 - "Heuristic Expiration" + if (!entry.containsHeader(HeaderConstants.WARNING)) { + final Header header = entry.getFirstHeader(HttpHeaders.DATE); + if (header != null) { + final Instant responseDate = DateUtils.parseStandardDate(header.getValue()); + final TimeValue freshnessLifetime = validityPolicy.getFreshnessLifetime(entry); + final TimeValue currentAge = validityPolicy.getCurrentAge(entry, responseDate); + if (freshnessLifetime.compareTo(ONE_DAY) > 0 && currentAge.compareTo(ONE_DAY) > 0) { + cachedResponse.addHeader(HeaderConstants.WARNING,"113 localhost \"Heuristic expiration\""); + if (LOG.isDebugEnabled()) { + LOG.debug("Added Warning 113 - Heuristic expiration to the response header."); + } + } + } + } + return cachedResponse; } 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 5431f3798..1395f6cc0 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 @@ -39,10 +39,12 @@ 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 java.util.List; import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; @@ -1500,6 +1502,14 @@ public class TestCachingExecChain { Mockito.when(cacheableRequestPolicy.isServableFromCache(Mockito.any())).thenReturn(true); Mockito.when(validityPolicy.getStaleness(Mockito.any(), Mockito.any())).thenReturn(TimeValue.MAX_VALUE); + // Assuming validityPolicy is a Mockito mock + Mockito.when(validityPolicy.getCurrentAge(Mockito.any(), Mockito.any())) + .thenReturn(TimeValue.ofMilliseconds(0)); + + // Assuming validityPolicy is a Mockito mock + Mockito.when(validityPolicy.getFreshnessLifetime(Mockito.any())) + .thenReturn(TimeValue.ofMilliseconds(0)); + final SimpleHttpResponse response = SimpleHttpResponse.create(HttpStatus.SC_OK); final AtomicInteger callCount = new AtomicInteger(0); @@ -1558,6 +1568,7 @@ public class TestCachingExecChain { Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1); + execute(req1); Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2); @@ -1569,4 +1580,75 @@ public class TestCachingExecChain { Mockito.verify(mockExecChain, Mockito.times(5)).proceed(Mockito.any(), Mockito.any()); } + @Test + public void testHeuristicExpirationWarning() 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); + + // Assuming validityPolicy is a Mockito mock + Mockito.when(validityPolicy.getCurrentAge(Mockito.any(), Mockito.any())) + .thenReturn(TimeValue.ofDays(2)); + + // Assuming validityPolicy is a Mockito mock + Mockito.when(validityPolicy.getFreshnessLifetime(Mockito.any())) + .thenReturn(TimeValue.ofDays(2)); + + 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()); + + // For example, you can check for the "Warning" header indicating a stale response: + final List
warningHeaders = Arrays.stream(response.getHeaders()) + .filter(header -> header.getName().equalsIgnoreCase("Warning")) + .collect(Collectors.toList()); + + + Assertions.assertFalse(warningHeaders.isEmpty()); + + final boolean found113 = warningHeaders.stream().anyMatch(header -> header.getValue().contains("113 localhost \"Heuristic expiration\"")); + Assertions.assertTrue(found113); + + final boolean found110 = warningHeaders.stream().anyMatch(header -> header.getValue().contains("110 localhost \"Response is stale\"")); + Assertions.assertTrue(found110); + } + }