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