Refactor CacheControl parser to handle multiple headers.

This commit refactors the CacheControl parsing logic to handle multiple "Cache-Control" headers. The previous implementation treated each header independently, returning an array of CacheControl objects. This caused issues when headers had directives that should be combined into a single CacheControl object.
The updated implementation combines all directives from all headers into a single CacheControl object, ensuring accurate representation of the caching directives.
This commit is contained in:
Arturo Bernal 2023-05-13 22:02:45 +02:00 committed by Oleg Kalnichevski
parent cf7b582d6e
commit 46fe5a6a81
5 changed files with 117 additions and 85 deletions

View File

@ -28,6 +28,7 @@ package org.apache.hc.client5.http.impl.cache;
import java.util.BitSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.hc.client5.http.cache.HeaderConstants;
@ -50,7 +51,7 @@ import org.slf4j.LoggerFactory;
* This class is thread-safe and has a singleton instance ({@link #INSTANCE}).
* </p>
* <p>
* The {@link #parse(Header)} method takes an HTTP header and returns a {@link CacheControl} object containing
* The {@link #parse(Iterator)} method takes an HTTP header and returns a {@link CacheControl} object containing
* the relevant caching directives. The header can be either a {@link FormattedHeader} object, which contains a
* pre-parsed {@link CharArrayBuffer}, or a plain {@link Header} object, in which case the value will be parsed and
* stored in a new {@link CharArrayBuffer}.
@ -113,28 +114,14 @@ class CacheControlHeaderParser {
* <p>
* "s-maxage" (-1).</p>
*
* @param header the header to parse, cannot be {@code null}
* @param headerIterator the header to parse, cannot be {@code null}
* @return a new {@link CacheControl} instance containing the relevant caching directives parsed from the header
* @throws IllegalArgumentException if the input header is {@code null}
*/
public final CacheControl parse(final Header header) {
Args.notNull(header, "Header");
final CharArrayBuffer buffer;
final Tokenizer.Cursor cursor;
if (header instanceof FormattedHeader) {
buffer = ((FormattedHeader) header).getBuffer();
cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length());
} else {
final String s = header.getValue();
if (s == null) {
return new CacheControl();
}
buffer = new CharArrayBuffer(s.length());
buffer.append(s);
cursor = new Tokenizer.Cursor(0, buffer.length());
}
public final CacheControl parse(final Iterator<Header> headerIterator) {
Args.notNull(headerIterator, "headerIterator");
// Initialize variables to hold the Cache-Control directives
long maxAge = -1;
long sharedMaxAge = -1;
boolean noCache = false;
@ -144,55 +131,76 @@ class CacheControlHeaderParser {
boolean proxyRevalidate = false;
boolean cachePublic = false;
long staleWhileRevalidate = -1;
// Declare a new Set variable at the beginning of the method to store the field-names
final Set<String> noCacheFields = new HashSet<>();
while (!cursor.atEnd()) {
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
String value = null;
if (!cursor.atEnd()) {
final int valueDelim = buffer.charAt(cursor.getPos());
cursor.updatePos(cursor.getPos() + 1);
if (valueDelim == EQUAL_CHAR) {
value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
if (!cursor.atEnd()) {
cursor.updatePos(cursor.getPos() + 1);
}
// Iterate over each header
while (headerIterator.hasNext()) {
final Header header = headerIterator.next();
final CharArrayBuffer buffer;
final Tokenizer.Cursor cursor;
if (header instanceof FormattedHeader) {
buffer = ((FormattedHeader) header).getBuffer();
cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length());
} else {
final String s = header.getValue();
if (s == null) {
return new CacheControl();
}
buffer = new CharArrayBuffer(s.length());
buffer.append(s);
cursor = new Tokenizer.Cursor(0, buffer.length());
}
if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_S_MAX_AGE)) {
sharedMaxAge = parseSeconds(name, value);
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) {
maxAge = parseSeconds(name, value);
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE)) {
mustRevalidate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) {
noCache = true;
if (value != null) {
final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length());
while (!valCursor.atEnd()) {
final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS);
if (!TextUtils.isBlank(token)) {
noCacheFields.add(token);
}
if (!valCursor.atEnd()) {
valCursor.updatePos(valCursor.getPos() + 1);
// Parse the header
while (!cursor.atEnd()) {
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
String value = null;
if (!cursor.atEnd()) {
final int valueDelim = buffer.charAt(cursor.getPos());
cursor.updatePos(cursor.getPos() + 1);
if (valueDelim == EQUAL_CHAR) {
value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
if (!cursor.atEnd()) {
cursor.updatePos(cursor.getPos() + 1);
}
}
}
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) {
noStore = true;
} else if (name.equalsIgnoreCase(HeaderConstants.PRIVATE)) {
cachePrivate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE)) {
proxyRevalidate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) {
cachePublic = true;
} else if (name.equalsIgnoreCase(HeaderConstants.STALE_WHILE_REVALIDATE)) {
staleWhileRevalidate = parseSeconds(name, value);
// Update the Cache-Control directives based on the current header
if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_S_MAX_AGE)) {
sharedMaxAge = parseSeconds(name, value);
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) {
maxAge = parseSeconds(name, value);
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE)) {
mustRevalidate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) {
noCache = true;
if (value != null) {
final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length());
while (!valCursor.atEnd()) {
final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS);
if (!TextUtils.isBlank(token)) {
noCacheFields.add(token);
}
if (!valCursor.atEnd()) {
valCursor.updatePos(valCursor.getPos() + 1);
}
}
}
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) {
noStore = true;
} else if (name.equalsIgnoreCase(HeaderConstants.PRIVATE)) {
cachePrivate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE)) {
proxyRevalidate = true;
} else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) {
cachePublic = true;
} else if (name.equalsIgnoreCase(HeaderConstants.STALE_WHILE_REVALIDATE)) {
staleWhileRevalidate = parseSeconds(name, value);
}
}
}
// Return a single CacheControl object with the combined directives
return new CacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate, cachePublic, staleWhileRevalidate, noCacheFields);
}

View File

@ -383,11 +383,11 @@ public class CachingExecBase {
* false otherwise
*/
boolean requestContainsNoCacheDirective(final HttpRequest request) {
final Header cacheControlHeader = request.getFirstHeader(HttpHeaders.CACHE_CONTROL);
if (cacheControlHeader == null) {
final Iterator<Header> it = request.headerIterator(HttpHeaders.CACHE_CONTROL);
if (it == null || !it.hasNext()) {
return false;
} else {
final CacheControl cacheControl = CacheControlHeaderParser.INSTANCE.parse(cacheControlHeader);
final CacheControl cacheControl = CacheControlHeaderParser.INSTANCE.parse(it);
return cacheControl.isNoCache();
}
}

View File

@ -535,11 +535,11 @@ class ResponseCachingPolicy {
* @return a CacheControl instance with the parsed directives or default values if the header is not present
*/
private CacheControl parseCacheControlHeader(final MessageHeaders messageHeaders) {
final Header cacheControlHeader = messageHeaders.getFirstHeader(HttpHeaders.CACHE_CONTROL);
if (cacheControlHeader == null) {
final Iterator<Header> it = messageHeaders.headerIterator(HttpHeaders.CACHE_CONTROL);
if (it == null || !it.hasNext()) {
return null;
} else {
return CacheControlHeaderParser.INSTANCE.parse(cacheControlHeader);
return CacheControlHeaderParser.INSTANCE.parse(it);
}
}

View File

@ -35,6 +35,9 @@ import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
public class CacheControlParserTest {
private final CacheControlHeaderParser parser = CacheControlHeaderParser.INSTANCE;
@ -42,69 +45,69 @@ public class CacheControlParserTest {
@Test
public void testParseMaxAgeZero() {
final Header header = new BasicHeader("Cache-Control", "max-age=0 , this = stuff;");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(0L, cacheControl.getMaxAge());
}
@Test
public void testParseSMaxAge() {
final Header header = new BasicHeader("Cache-Control", "s-maxage=3600");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(3600L, cacheControl.getSharedMaxAge());
}
@Test
public void testParseInvalidCacheValue() {
final Header header = new BasicHeader("Cache-Control", "max-age=invalid");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testParseInvalidHeader() {
final Header header = new BasicHeader("Cache-Control", "max-age");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testParseNullHeader() {
final Header header = null;
assertThrows(NullPointerException.class, () -> parser.parse(header));
assertThrows(NullPointerException.class, () -> parser.parse(Collections.singletonList(header).iterator()));
}
@Test
public void testParseEmptyHeader() {
final Header header = new BasicHeader("Cache-Control", "");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testParseCookieEmptyValue() {
final Header header = new BasicHeader("Cache-Control", "max-age=;");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testParseNoCache() {
final Header header = new BasicHeader(" Cache-Control", "no-cache");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testParseNoDirective() {
final Header header = new BasicHeader(" Cache-Control", "");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@Test
public void testGarbage() {
final Header header = new BasicHeader("Cache-Control", ",,= blah,");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(-1L, cacheControl.getMaxAge());
}
@ -112,7 +115,7 @@ public class CacheControlParserTest {
@Test
public void testParseMultipleDirectives() {
final Header header = new BasicHeader("Cache-Control", "max-age=604800, stale-while-revalidate=86400, s-maxage=3600, must-revalidate, private");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertAll("Must all pass",
() -> assertEquals(604800L, cacheControl.getMaxAge()),
@ -125,7 +128,7 @@ public class CacheControlParserTest {
@Test
public void testParseMultipleDirectives2() {
final Header header = new BasicHeader("Cache-Control", "max-age=604800, stale-while-revalidate=86400, must-revalidate, private, s-maxage=3600");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertAll("Must all pass",
() -> assertEquals(604800L, cacheControl.getMaxAge()),
@ -138,7 +141,7 @@ public class CacheControlParserTest {
@Test
public void testParsePublic() {
final Header header = new BasicHeader("Cache-Control", "public");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isPublic());
}
@ -146,7 +149,7 @@ public class CacheControlParserTest {
@Test
public void testParsePrivate() {
final Header header = new BasicHeader("Cache-Control", "private");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isCachePrivate());
}
@ -154,7 +157,7 @@ public class CacheControlParserTest {
@Test
public void testParseNoStore() {
final Header header = new BasicHeader("Cache-Control", "no-store");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isNoStore());
}
@ -162,7 +165,7 @@ public class CacheControlParserTest {
@Test
public void testParseStaleWhileRevalidate() {
final Header header = new BasicHeader("Cache-Control", "max-age=3600, stale-while-revalidate=120");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertEquals(120, cacheControl.getStaleWhileRevalidate());
}
@ -170,7 +173,7 @@ public class CacheControlParserTest {
@Test
public void testParseNoCacheFields() {
final Header header = new BasicHeader("Cache-Control", "no-cache=\"Set-Cookie, Content-Language\", stale-while-revalidate=120");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isNoCache());
assertEquals(2, cacheControl.getNoCacheFields().size());
@ -182,7 +185,7 @@ public class CacheControlParserTest {
@Test
public void testParseNoCacheFieldsNoQuote() {
final Header header = new BasicHeader("Cache-Control", "no-cache=Set-Cookie, Content-Language, stale-while-revalidate=120");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isNoCache());
assertEquals(1, cacheControl.getNoCacheFields().size());
@ -193,7 +196,7 @@ public class CacheControlParserTest {
@Test
public void testParseNoCacheFieldsMessy() {
final Header header = new BasicHeader("Cache-Control", "no-cache=\" , , ,,, Set-Cookie , , Content-Language , \", stale-while-revalidate=120");
final CacheControl cacheControl = parser.parse(header);
final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator());
assertTrue(cacheControl.isNoCache());
assertEquals(2, cacheControl.getNoCacheFields().size());
@ -202,4 +205,25 @@ public class CacheControlParserTest {
assertEquals(120, cacheControl.getStaleWhileRevalidate());
}
@Test
public void testParseMultipleHeaders() {
// Create headers
final Header header1 = new BasicHeader("Cache-Control", "max-age=3600, no-store");
final Header header2 = new BasicHeader("Cache-Control", "private, must-revalidate");
final Header header3 = new BasicHeader("Cache-Control", "max-age=3600");
final Header header4 = new BasicHeader("Cache-Control", "no-store");
final Header header5 = new BasicHeader("Cache-Control", "private");
final Header header6 = new BasicHeader("Cache-Control", "must-revalidate");
// Parse headers
final CacheControl cacheControl1 = parser.parse(Arrays.asList(header1, header2).iterator());
final CacheControl cacheControl2 = parser.parse(Arrays.asList(header3, header4, header5, header6).iterator());
// Validate Cache-Control directives
assertEquals(cacheControl1.getMaxAge(), cacheControl2.getMaxAge());
assertEquals(cacheControl1.isNoStore(), cacheControl2.isNoStore());
assertEquals(cacheControl1.isCachePrivate(), cacheControl2.isCachePrivate());
assertEquals(cacheControl1.isMustRevalidate(), cacheControl2.isMustRevalidate());
}
}

View File

@ -381,7 +381,7 @@ public class TestResponseCachingPolicy {
response.addHeader("Cache-Control", "max-age=20");
response.addHeader("Cache-Control", "public, no-store");
Assertions.assertTrue(policy.isResponseCacheable("GET", response));
Assertions.assertFalse(policy.isResponseCacheable("GET", response));
}
@Test
@ -389,7 +389,7 @@ public class TestResponseCachingPolicy {
response.addHeader("Cache-Control", "max-age=20");
response.addHeader("Cache-Control", "public, no-store");
Assertions.assertTrue(policy.isResponseCacheable("HEAD", response));
Assertions.assertFalse(policy.isResponseCacheable("HEAD", response));
}
@Test