You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2023/07/24 20:36:52 UTC
[httpcomponents-client] branch 5.3.x updated: Implement Cache-Control Extension in Response Caching Policy. (#462)
This is an automated email from the ASF dual-hosted git repository.
abernal pushed a commit to branch 5.3.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git
The following commit(s) were added to refs/heads/5.3.x by this push:
new f61fb5138 Implement Cache-Control Extension in Response Caching Policy. (#462)
f61fb5138 is described below
commit f61fb51383e111ac6ef0290e1e3628c2d5009cd0
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Mon Jul 24 17:36:47 2023 -0300
Implement Cache-Control Extension in Response Caching Policy. (#462)
This commit adds the functionality to handle the 'immutable' directive in the Cache-Control header as per the RFC8246 specifications.
Key changes include:
- The 'immutable' directive is checked in the Cache-Control of an HTTP response, indicating that the origin server will not update the resource representation during the response's freshness lifetime.
- If the 'immutable' directive is present and the response is still fresh, the response is considered cacheable without further validation.
- Ignoring any arguments with the 'immutable' directive, as per RFC stipulations.
- Treating multiple instances of the 'immutable' directive as equivalent to one.
---
.../hc/client5/http/cache/HeaderConstants.java | 1 +
.../hc/client5/http/impl/cache/CacheControl.java | 1 -
.../http/impl/cache/CacheControlHeaderParser.java | 2 +
.../http/impl/cache/RequestCacheControl.java | 24 ++++++++++-
.../http/impl/cache/ResponseCacheControl.java | 49 +++++++++++++++++++++-
.../http/impl/cache/ResponseCachingPolicy.java | 38 ++++++++++++++++-
.../http/impl/cache/CacheControlParserTest.java | 14 +++++++
.../http/impl/cache/TestResponseCachingPolicy.java | 10 +++++
8 files changed, 133 insertions(+), 6 deletions(-)
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java
index 60c0ba5d1..a04352e48 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java
@@ -165,6 +165,7 @@ public class HeaderConstants {
public static final String CACHE_CONTROL_STALE_WHILE_REVALIDATE = "stale-while-revalidate";
public static final String CACHE_CONTROL_ONLY_IF_CACHED = "only-if-cached";
public static final String CACHE_CONTROL_MUST_UNDERSTAND = "must-understand";
+ public static final String CACHE_CONTROL_IMMUTABLE= "immutable";
/**
* @deprecated Use {@link #CACHE_CONTROL_STALE_IF_ERROR}
*/
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 8ad37633d..d451a4a19 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
@@ -68,5 +68,4 @@ interface CacheControl {
* @return The stale-if-error value.
*/
long getStaleIfError();
-
}
\ 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 f912c3146..dea42089a 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
@@ -198,6 +198,8 @@ class CacheControlHeaderParser {
builder.setStaleIfError(parseSeconds(name, value));
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_UNDERSTAND)) {
builder.setMustUnderstand(true);
+ } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_IMMUTABLE)) {
+ builder.setImmutable(true);
}
});
return builder.build();
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java
index a1f442ab1..1df87ac40 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java
@@ -49,8 +49,15 @@ final class RequestCacheControl implements CacheControl {
private final boolean onlyIfCached;
private final long staleIfError;
+ /**
+ * Flag for the 'no-transform' Cache-Control directive.
+ * If this field is true, then the 'no-transform' directive is present in the Cache-Control header.
+ * According to RFC 'no-transform' directive indicates that the cache MUST NOT transform the payload.
+ */
+ private final boolean noTransform;
+
RequestCacheControl(final long maxAge, final long maxStale, final long minFresh, final boolean noCache,
- final boolean noStore, final boolean onlyIfCached, final long staleIfError) {
+ final boolean noStore, final boolean onlyIfCached, final long staleIfError, final boolean noTransform) {
this.maxAge = maxAge;
this.maxStale = maxStale;
this.minFresh = minFresh;
@@ -58,6 +65,7 @@ final class RequestCacheControl implements CacheControl {
this.noStore = noStore;
this.onlyIfCached = onlyIfCached;
this.staleIfError = staleIfError;
+ this.noTransform = noTransform;
}
/**
@@ -137,6 +145,7 @@ final class RequestCacheControl implements CacheControl {
", noStore=" + noStore +
", onlyIfCached=" + onlyIfCached +
", staleIfError=" + staleIfError +
+ ", noTransform=" + noTransform +
'}';
}
@@ -153,6 +162,7 @@ final class RequestCacheControl implements CacheControl {
private boolean noStore;
private boolean onlyIfCached;
private long staleIfError = -1;
+ private boolean noTransform;
Builder() {
}
@@ -220,8 +230,18 @@ final class RequestCacheControl implements CacheControl {
return this;
}
+ public boolean isNoTransform() {
+ return noTransform;
+ }
+
+ public Builder setNoTransform(final boolean noTransform) {
+ this.noTransform = noTransform;
+ return this;
+ }
+
+
public RequestCacheControl build() {
- return new RequestCacheControl(maxAge, maxStale, minFresh, noCache, noStore, onlyIfCached, staleIfError);
+ return new RequestCacheControl(maxAge, maxStale, minFresh, noCache, noStore, onlyIfCached, staleIfError, noTransform);
}
}
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java
index 866757c00..33469b533 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java
@@ -106,6 +106,14 @@ final class ResponseCacheControl implements CacheControl {
private final boolean undefined;
+ /**
+ * Flag for the 'immutable' Cache-Control directive.
+ * If this field is true, then the 'immutable' directive is present in the Cache-Control header.
+ * The 'immutable' directive is meant to inform a cache or user agent that the response body will not
+ * change over time, even though it may be requested multiple times.
+ */
+ private final boolean immutable;
+
/**
* Creates a new instance of {@code CacheControl} with the specified values.
*
@@ -121,11 +129,12 @@ final class ResponseCacheControl implements CacheControl {
* @param staleIfError The stale-if-error value from the Cache-Control header.
* @param noCacheFields The set of field names specified in the "no-cache" directive of the Cache-Control header.
* @param mustUnderstand The must-understand value from the Cache-Control header.
+ * @param immutable The immutable value from the Cache-Control header.
*/
ResponseCacheControl(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 long staleWhileRevalidate, final long staleIfError,
- final Set<String> noCacheFields, final boolean mustUnderstand) {
+ final Set<String> noCacheFields, final boolean mustUnderstand, final boolean immutable) {
this.maxAge = maxAge;
this.sharedMaxAge = sharedMaxAge;
this.noCache = noCache;
@@ -148,6 +157,7 @@ final class ResponseCacheControl implements CacheControl {
staleWhileRevalidate == -1
&& staleIfError == -1;
this.mustUnderstand = mustUnderstand;
+ this.immutable = immutable;
}
/**
@@ -263,10 +273,24 @@ final class ResponseCacheControl implements CacheControl {
return noCacheFields;
}
+ /**
+ * Returns the 'immutable' Cache-Control directive status.
+ *
+ * @return true if the 'immutable' directive is present in the Cache-Control header.
+ */
public boolean isUndefined() {
return undefined;
}
+ /**
+ * Returns the 'immutable' Cache-Control directive status.
+ *
+ * @return true if the 'immutable' directive is present in the Cache-Control header.
+ */
+ public boolean isImmutable() {
+ return immutable;
+ }
+
@Override
public String toString() {
return "CacheControl{" +
@@ -282,6 +306,7 @@ final class ResponseCacheControl implements CacheControl {
", staleIfError=" + staleIfError +
", noCacheFields=" + noCacheFields +
", mustUnderstand=" + mustUnderstand +
+ ", immutable=" + immutable +
'}';
}
@@ -303,6 +328,8 @@ final class ResponseCacheControl implements CacheControl {
private long staleIfError = -1;
private Set<String> noCacheFields;
private boolean mustUnderstand;
+ private boolean noTransform;
+ private boolean immutable;
Builder() {
}
@@ -415,9 +442,27 @@ final class ResponseCacheControl implements CacheControl {
return this;
}
+ public boolean isNoTransform() {
+ return noStore;
+ }
+
+ public Builder setNoTransform(final boolean noTransform) {
+ this.noTransform = noTransform;
+ return this;
+ }
+
+ public boolean isImmutable() {
+ return immutable;
+ }
+
+ public Builder setImmutable(final boolean immutable) {
+ this.immutable = immutable;
+ return this;
+ }
+
public ResponseCacheControl build() {
return new ResponseCacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate,
- cachePublic, staleWhileRevalidate, staleIfError, noCacheFields, mustUnderstand);
+ cachePublic, staleWhileRevalidate, staleIfError, noCacheFields, mustUnderstand, immutable);
}
}
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 fab185b4a..8237927fc 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
@@ -231,8 +231,18 @@ class ResponseCachingPolicy {
return false;
}
- // calculate freshness lifetime
final Duration freshnessLifetime = calculateFreshnessLifetime(cacheControl, response);
+
+ // If the 'immutable' directive is present and the response is still fresh,
+ // then the response is considered cacheable without further validation
+ if (cacheControl.isImmutable() && responseIsStillFresh(response, freshnessLifetime)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Response is immutable and fresh, considered cacheable without further validation");
+ }
+ return true;
+ }
+
+ // calculate freshness lifetime
if (freshnessLifetime.isNegative() || freshnessLifetime.isZero()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Freshness lifetime is invalid");
@@ -521,4 +531,30 @@ class ResponseCachingPolicy {
(status >= 500 && status <= 505);
}
+ /**
+ * Determines if an HttpResponse is still fresh based on its Date header and calculated freshness lifetime.
+ *
+ * <p>
+ * This method calculates the age of the response from its Date header and compares it with the provided freshness
+ * lifetime. If the age is less than the freshness lifetime, the response is considered fresh.
+ * </p>
+ *
+ * <p>
+ * Note: If the Date header is missing or invalid, this method assumes the response is not fresh.
+ * </p>
+ *
+ * @param response The HttpResponse whose freshness is being checked.
+ * @param freshnessLifetime The calculated freshness lifetime of the HttpResponse.
+ * @return {@code true} if the response age is less than its freshness lifetime, {@code false} otherwise.
+ */
+ private boolean responseIsStillFresh(final HttpResponse response, final Duration freshnessLifetime) {
+ final Instant date = DateUtils.parseStandardDate(response, HttpHeaders.DATE);
+ if (date == null) {
+ // The Date header is missing or invalid. Assuming the response is not fresh.
+ return false;
+ }
+ final Duration age = Duration.between(date, Instant.now());
+ return age.compareTo(freshnessLifetime) < 0;
+ }
+
}
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 66c892c48..31a7bc109 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
@@ -243,4 +243,18 @@ public class CacheControlParserTest {
);
}
+ @Test
+ public void testParseIsImmutable() {
+ final Header header = new BasicHeader("Cache-Control", "max-age=0 , immutable");
+ final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator());
+ assertTrue(cacheControl.isImmutable());
+ }
+
+ @Test
+ public void testParseMultipleIsImmutable() {
+ final Header header = new BasicHeader("Cache-Control", "immutable, nmax-age=0 , immutable");
+ final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator());
+ assertTrue(cacheControl.isImmutable());
+ }
+
}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java
index 57344e31c..7ec81cdca 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java
@@ -1080,4 +1080,14 @@ public class TestResponseCachingPolicy {
.build();
assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
}
+
+ @Test
+ public void testImmutableAndFreshResponseIsCacheable() {
+ responseCacheControl = ResponseCacheControl.builder()
+ .setImmutable(true)
+ .setMaxAge(3600) // set this to a value that ensures the response is still fresh
+ .build();
+
+ Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, "GET", response));
+ }
}
\ No newline at end of file