HTTPCLIENT-2277: Improve Age Header Handling and Calculation in Accordance with RFC9111.
This commit enhances the processing and calculation of the "Age" response header field to comply fully with Sections 5.1 and 4.2.3 of RFC9111.
This commit is contained in:
parent
f8eb716c11
commit
e26896596f
|
@ -28,6 +28,7 @@ package org.apache.hc.client5.http.impl.cache;
|
|||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.hc.client5.http.cache.HttpCacheEntry;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
|
@ -206,28 +207,53 @@ class CacheValidityPolicy {
|
|||
return TimeValue.ofSeconds(diff.getSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and processes the Age value from an HttpCacheEntry by tokenizing the Age header value.
|
||||
* The Age header value is interpreted as a sequence of tokens, and the first token is parsed into a number
|
||||
* representing the age in delta-seconds. If the first token cannot be parsed into a number, the Age value is
|
||||
* considered as invalid and this method returns 0. If the first token represents a negative number or a number
|
||||
* that exceeds Integer.MAX_VALUE, the Age value is set to MAX_AGE (in seconds).
|
||||
* This method uses CacheSupport.parseTokens to robustly handle the Age header value.
|
||||
* <p>
|
||||
* Note: If the HttpCacheEntry contains multiple Age headers, only the first one is considered.
|
||||
*
|
||||
* @param entry The HttpCacheEntry from which to extract the Age value.
|
||||
* @return The Age value in delta-seconds, or MAX_AGE in seconds if the Age value exceeds Integer.MAX_VALUE or
|
||||
* is negative. If the Age value is invalid (cannot be parsed into a number or contains non-numeric characters),
|
||||
* this method returns 0.
|
||||
*/
|
||||
protected long getAgeValue(final HttpCacheEntry entry) {
|
||||
// This is a header value, we leave as-is
|
||||
long ageValue = 0;
|
||||
for (final Header hdr : entry.getHeaders(HttpHeaders.AGE)) {
|
||||
long hdrAge;
|
||||
final Header age = entry.getFirstHeader(HttpHeaders.AGE);
|
||||
if (age != null) {
|
||||
try {
|
||||
hdrAge = Long.parseLong(hdr.getValue());
|
||||
if (hdrAge < 0) {
|
||||
hdrAge = MAX_AGE.toSeconds();
|
||||
final AtomicReference<String> firstToken = new AtomicReference<>();
|
||||
CacheSupport.parseTokens(age, token -> firstToken.compareAndSet(null, token));
|
||||
final String s = firstToken.get();
|
||||
if (s != null) {
|
||||
long ageValue = Long.parseLong(s);
|
||||
if (ageValue < 0) {
|
||||
ageValue = 0; // Handle negative age values as invalid
|
||||
} else if (ageValue > Integer.MAX_VALUE) {
|
||||
ageValue = MAX_AGE.toSeconds();
|
||||
}
|
||||
return ageValue;
|
||||
}
|
||||
} catch (final NumberFormatException ex) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Invalid Age header: '{}'. Ignoring.", age, ex);
|
||||
}
|
||||
} catch (final NumberFormatException nfe) {
|
||||
hdrAge = MAX_AGE.toSeconds();
|
||||
}
|
||||
ageValue = (hdrAge > ageValue) ? hdrAge : ageValue;
|
||||
}
|
||||
return ageValue;
|
||||
// If we've got here, there were no valid Age headers
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected TimeValue getCorrectedReceivedAge(final HttpCacheEntry entry) {
|
||||
final TimeValue apparentAge = getApparentAge(entry);
|
||||
|
||||
|
||||
protected TimeValue getCorrectedAgeValue(final HttpCacheEntry entry) {
|
||||
final long ageValue = getAgeValue(entry);
|
||||
return (apparentAge.toSeconds() > ageValue) ? apparentAge : TimeValue.ofSeconds(ageValue);
|
||||
final long responseDelay = getResponseDelay(entry).toSeconds();
|
||||
return TimeValue.ofSeconds(ageValue + responseDelay);
|
||||
}
|
||||
|
||||
protected TimeValue getResponseDelay(final HttpCacheEntry entry) {
|
||||
|
@ -236,7 +262,9 @@ class CacheValidityPolicy {
|
|||
}
|
||||
|
||||
protected TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
|
||||
return TimeValue.ofSeconds(getCorrectedReceivedAge(entry).toSeconds() + getResponseDelay(entry).toSeconds());
|
||||
final long apparentAge = getApparentAge(entry).toSeconds();
|
||||
final long correctedReceivedAge = getCorrectedAgeValue(entry).toSeconds();
|
||||
return TimeValue.ofSeconds(Math.max(apparentAge, correctedReceivedAge));
|
||||
}
|
||||
|
||||
protected TimeValue getResidentTime(final HttpCacheEntry entry, final Instant now) {
|
||||
|
|
|
@ -201,11 +201,6 @@ class ResponseCachingPolicy {
|
|||
}
|
||||
}
|
||||
|
||||
if (response.countHeaders(HttpHeaders.AGE) > 1) {
|
||||
LOG.debug("Multiple Age headers");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.countHeaders(HttpHeaders.EXPIRES) > 1) {
|
||||
LOG.debug("Multiple Expires headers");
|
||||
return false;
|
||||
|
|
|
@ -95,20 +95,14 @@ public class TestCacheValidityPolicy {
|
|||
return TimeValue.ofSeconds(6);
|
||||
}
|
||||
};
|
||||
assertEquals(TimeValue.ofSeconds(10), impl.getCorrectedReceivedAge(entry));
|
||||
assertEquals(TimeValue.ofSeconds(10), impl.getCorrectedAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCorrectedReceivedAgeIsApparentAgeIfLarger() {
|
||||
public void testGetCorrectedAgeValue() {
|
||||
final Header[] headers = new Header[] { new BasicHeader("Age", "6"), };
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
|
||||
impl = new CacheValidityPolicy() {
|
||||
@Override
|
||||
protected TimeValue getApparentAge(final HttpCacheEntry ent) {
|
||||
return TimeValue.ofSeconds(10);
|
||||
}
|
||||
};
|
||||
assertEquals(TimeValue.ofSeconds(10), impl.getCorrectedReceivedAge(entry));
|
||||
assertEquals(TimeValue.ofSeconds(6), impl.getCorrectedAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -122,7 +116,7 @@ public class TestCacheValidityPolicy {
|
|||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
|
||||
impl = new CacheValidityPolicy() {
|
||||
@Override
|
||||
protected TimeValue getCorrectedReceivedAge(final HttpCacheEntry ent) {
|
||||
protected TimeValue getCorrectedAgeValue(final HttpCacheEntry ent) {
|
||||
return TimeValue.ofSeconds(7);
|
||||
}
|
||||
|
||||
|
@ -131,7 +125,7 @@ public class TestCacheValidityPolicy {
|
|||
return TimeValue.ofSeconds(13);
|
||||
}
|
||||
};
|
||||
assertEquals(TimeValue.ofSeconds(20), impl.getCorrectedInitialAge(entry));
|
||||
assertEquals(TimeValue.ofSeconds(7), impl.getCorrectedInitialAge(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -345,11 +339,11 @@ public class TestCacheValidityPolicy {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeAgeHeaderValueReturnsMaxAge() {
|
||||
public void testNegativeAgeHeaderValueReturnsZero() {
|
||||
final Header[] headers = new Header[] { new BasicHeader("Age", "-100") };
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(headers);
|
||||
// in seconds
|
||||
assertEquals(CacheValidityPolicy.MAX_AGE.toSeconds(), impl.getAgeValue(entry));
|
||||
assertEquals(0, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -357,9 +351,43 @@ public class TestCacheValidityPolicy {
|
|||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
new BasicHeader("Age", "asdf"));
|
||||
// in seconds
|
||||
assertEquals(CacheValidityPolicy.MAX_AGE.toSeconds(), impl.getAgeValue(entry));
|
||||
assertEquals(0, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedAgeHeaderMultipleWellFormedAges() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
new BasicHeader("Age", "123,456,789"));
|
||||
// in seconds
|
||||
assertEquals(123, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedAgeHeaderMultiplesMalformedAges() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
new BasicHeader("Age", "123 456 789"));
|
||||
// in seconds
|
||||
assertEquals(0, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedAgeHeaderNegativeAge() {
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
new BasicHeader("Age", "-123"));
|
||||
// in seconds
|
||||
assertEquals(0, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedAgeHeaderOverflow() {
|
||||
final String reallyOldAge = "1" + Long.MAX_VALUE;
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
|
||||
new BasicHeader("Age", reallyOldAge));
|
||||
// Expect the age value to be 0 in case of overflow
|
||||
assertEquals(0, impl.getAgeValue(entry));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMayReturnStaleIfErrorInResponseIsTrueWithinStaleness(){
|
||||
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(now, now,
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
*/
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
|
@ -2123,7 +2125,13 @@ public class TestProtocolRequirements {
|
|||
final ClassicHttpResponse result = execute(request);
|
||||
|
||||
Assertions.assertEquals(200, result.getCode());
|
||||
Assertions.assertEquals("11", result.getFirstHeader("Age").getValue());
|
||||
// We calculate the age of the cache entry as per RFC 9111:
|
||||
// We first find the "corrected_initial_age" which is the maximum of "apparentAge" and "correctedReceivedAge".
|
||||
// In this case, max(1, 2) = 2 seconds.
|
||||
// We then add the "residentTime" which is "now - responseTime",
|
||||
// which is the current time minus the time the cache entry was created. In this case, that is 8 seconds.
|
||||
// So, the total age is "corrected_initial_age" + "residentTime" = 2 + 8 = 10 seconds.
|
||||
assertThat(result, ContainsHeaderMatcher.contains("Age", "10"));
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -4006,7 +4014,7 @@ public class TestProtocolRequirements {
|
|||
|
||||
final ClassicHttpResponse result = execute(request);
|
||||
|
||||
Assertions.assertEquals("2147483648",
|
||||
Assertions.assertEquals(reallyOldAge,
|
||||
result.getFirstHeader("Age").getValue());
|
||||
}
|
||||
|
||||
|
|
|
@ -530,10 +530,10 @@ public class TestResponseCachingPolicy {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testResponsesWithMultipleAgeHeadersAreNotCacheable() {
|
||||
public void testResponsesWithMultipleAgeHeadersAreCacheable() {
|
||||
response.addHeader("Age", "3");
|
||||
response.addHeader("Age", "5");
|
||||
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, "GET", response));
|
||||
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, "GET", response));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -546,7 +546,7 @@ public class TestResponseCachingPolicy {
|
|||
responseCacheControl = ResponseCacheControl.builder()
|
||||
.setCachePublic(true)
|
||||
.build();
|
||||
Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
|
||||
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in New Issue