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.
This commit is contained in:
parent
9bde706ae7
commit
561c949f78
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -68,5 +68,4 @@ interface CacheControl {
|
|||
* @return The stale-if-error value.
|
||||
*/
|
||||
long getStaleIfError();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue