Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,4 @@ interface CacheControl {
* @return The stale-if-error value.
*/
long getStaleIfError();

}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ public final ResponseCacheControl parseResponse(final Iterator<Header> headerIte
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,23 @@ 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;
this.noCache = noCache;
this.noStore = noStore;
this.onlyIfCached = onlyIfCached;
this.staleIfError = staleIfError;
this.noTransform = noTransform;
}

/**
Expand Down Expand Up @@ -137,6 +145,7 @@ public String toString() {
", noStore=" + noStore +
", onlyIfCached=" + onlyIfCached +
", staleIfError=" + staleIfError +
", noTransform=" + noTransform +
'}';
}

Expand All @@ -153,6 +162,7 @@ static class Builder {
private boolean noStore;
private boolean onlyIfCached;
private long staleIfError = -1;
private boolean noTransform;

Builder() {
}
Expand Down Expand Up @@ -220,8 +230,18 @@ public Builder setStaleIfError(final long staleIfError) {
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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
Expand All @@ -148,6 +157,7 @@ final class ResponseCacheControl implements CacheControl {
staleWhileRevalidate == -1
&& staleIfError == -1;
this.mustUnderstand = mustUnderstand;
this.immutable = immutable;
}

/**
Expand Down Expand Up @@ -263,10 +273,24 @@ public Set<String> getNoCacheFields() {
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{" +
Expand All @@ -282,6 +306,7 @@ public String toString() {
", staleIfError=" + staleIfError +
", noCacheFields=" + noCacheFields +
", mustUnderstand=" + mustUnderstand +
", immutable=" + immutable +
'}';
}

Expand All @@ -303,6 +328,8 @@ static class Builder {
private long staleIfError = -1;
private Set<String> noCacheFields;
private boolean mustUnderstand;
private boolean noTransform;
private boolean immutable;

Builder() {
}
Expand Down Expand Up @@ -415,9 +442,27 @@ public Builder setMustUnderstand(final boolean mustUnderstand) {
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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,18 @@ public boolean isResponseCacheable(final ResponseCacheControl cacheControl, fina
return false;
}

// calculate freshness lifetime
final Duration freshnessLifetime = calculateFreshnessLifetime(cacheControl, response);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturobernalg I cannot find immutable cache control directive mentioned anywhere in the RFC 9111. Am I missing something?

// 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");
Expand Down Expand Up @@ -521,4 +531,30 @@ private boolean understoodStatusCode(final int status) {
(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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,18 @@ public void testParseRequestMultipleDirectives() {
);
}

@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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1080,4 +1080,14 @@ void testIsResponseCacheableNoStore() {
.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));
}
}