Allow certain responses from a POST to be cacheable.
Update ResponseCachingPolicy to allow caching of responses to POST requests under certain circumstances, as specified in RFC 2616 and explained in more detail in draft-ietf-httpbis-p2-semantics-20#section-2.3.4. This change extends the cacheability of responses beyond GET and HEAD methods, improving the cache's efficiency and reducing network traffic.
This commit is contained in:
parent
019cf460ec
commit
cfcdd11cb6
|
@ -39,6 +39,7 @@ public class HeaderConstants {
|
|||
public static final String PUT_METHOD = "PUT";
|
||||
public static final String DELETE_METHOD = "DELETE";
|
||||
public static final String TRACE_METHOD = "TRACE";
|
||||
public static final String POST_METHOD = "POST";
|
||||
|
||||
public static final String LAST_MODIFIED = "Last-Modified";
|
||||
public static final String IF_MATCH = "If-Match";
|
||||
|
|
111
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
vendored
Normal file
111
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* ====================================================================
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
* ====================================================================
|
||||
*
|
||||
* This software consists of voluntary contributions made by many
|
||||
* individuals on behalf of the Apache Software Foundation. For more
|
||||
* information on the Apache Software Foundation, please see
|
||||
* <http://www.apache.org/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import org.apache.hc.core5.annotation.Contract;
|
||||
import org.apache.hc.core5.annotation.Internal;
|
||||
import org.apache.hc.core5.annotation.ThreadingBehavior;
|
||||
|
||||
/**
|
||||
* Represents the values of the Cache-Control header in an HTTP response, which indicate whether and for how long
|
||||
* the response can be cached by the client and intermediary proxies.
|
||||
* <p>
|
||||
* The class provides methods to retrieve the maximum age of the response and the maximum age that applies to shared
|
||||
* caches. The values are expressed in seconds, with -1 indicating that the value was not specified in the header.
|
||||
* <p>
|
||||
* Instances of this class are immutable, meaning that their values cannot be changed once they are set. To create an
|
||||
* instance, use one of the constructors that take the desired values as arguments. Alternatively, use the default
|
||||
* constructor to create an instance with both values set to -1, indicating that the header was not present in the
|
||||
* response.
|
||||
* <p>
|
||||
* Example usage:
|
||||
* <pre>
|
||||
* HttpResponse response = httpClient.execute(httpGet);
|
||||
* CacheControlHeader cacheControlHeader = CacheControlHeaderParser.INSTANCE.parse(response.getHeaders("Cache-Control"));
|
||||
* long maxAge = cacheControlHeader.getMaxAge();
|
||||
* long sharedMaxAge = cacheControlHeader.getSharedMaxAge();
|
||||
* </pre>
|
||||
* @since 5.3
|
||||
*/
|
||||
@Internal
|
||||
@Contract(threading = ThreadingBehavior.IMMUTABLE)
|
||||
final class CacheControl {
|
||||
|
||||
/**
|
||||
* The max-age directive value.
|
||||
*/
|
||||
private final long maxAge;
|
||||
/**
|
||||
* The shared-max-age directive value.
|
||||
*/
|
||||
private final long sharedMaxAge;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@code CacheControlHeader} with the specified max-age and shared-max-age values.
|
||||
*
|
||||
* @param maxAge The max-age value from the Cache-Control header.
|
||||
* @param sharedMaxAge The shared-max-age value from the Cache-Control header.
|
||||
*/
|
||||
public CacheControl(final long maxAge, final long sharedMaxAge) {
|
||||
this.maxAge = maxAge;
|
||||
this.sharedMaxAge = sharedMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the max-age value from the Cache-Control header.
|
||||
*
|
||||
* @return The max-age value.
|
||||
*/
|
||||
public long getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared-max-age value from the Cache-Control header.
|
||||
*
|
||||
* @return The shared-max-age value.
|
||||
*/
|
||||
public long getSharedMaxAge() {
|
||||
return sharedMaxAge;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string representation of the {@code CacheControlHeader} object, including the max-age and shared-max-age values.
|
||||
*
|
||||
* @return A string representation of the object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CacheControl{" +
|
||||
"maxAge=" + maxAge +
|
||||
", sharedMaxAge=" + sharedMaxAge +
|
||||
'}';
|
||||
}
|
||||
}
|
173
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
vendored
Normal file
173
httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
vendored
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* ====================================================================
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
* ====================================================================
|
||||
*
|
||||
* This software consists of voluntary contributions made by many
|
||||
* individuals on behalf of the Apache Software Foundation. For more
|
||||
* information on the Apache Software Foundation, please see
|
||||
* <http://www.apache.org/>.
|
||||
*
|
||||
*/
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
import org.apache.hc.core5.annotation.Contract;
|
||||
import org.apache.hc.core5.annotation.Internal;
|
||||
import org.apache.hc.core5.annotation.ThreadingBehavior;
|
||||
import org.apache.hc.core5.http.FormattedHeader;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.util.Args;
|
||||
import org.apache.hc.core5.util.CharArrayBuffer;
|
||||
import org.apache.hc.core5.util.Tokenizer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
/**
|
||||
* A parser for the HTTP Cache-Control header that can be used to extract information about caching directives.
|
||||
* <p>
|
||||
* 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 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}.
|
||||
* </p>
|
||||
* <p>
|
||||
* This parser only supports two directives: "max-age" and "s-maxage". If either of these directives are present in the
|
||||
* header, their values will be parsed and stored in the {@link CacheControl} object. If both directives are
|
||||
* present, the value of "s-maxage" takes precedence.
|
||||
* </p>
|
||||
*/
|
||||
@Internal
|
||||
@Contract(threading = ThreadingBehavior.SAFE)
|
||||
class CacheControlHeaderParser {
|
||||
|
||||
/**
|
||||
* The singleton instance of this parser.
|
||||
*/
|
||||
public static final CacheControlHeaderParser INSTANCE = new CacheControlHeaderParser();
|
||||
|
||||
/**
|
||||
* The logger for this class.
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CacheControlHeaderParser.class);
|
||||
|
||||
|
||||
/**
|
||||
* The character used to indicate a parameter's value in the Cache-Control header.
|
||||
*/
|
||||
private final static char EQUAL_CHAR = '=';
|
||||
|
||||
/**
|
||||
* The set of characters that can delimit a token in the header.
|
||||
*/
|
||||
private static final BitSet TOKEN_DELIMS = Tokenizer.INIT_BITSET(EQUAL_CHAR, ',');
|
||||
|
||||
/**
|
||||
* The set of characters that can delimit a value in the header.
|
||||
*/
|
||||
private static final BitSet VALUE_DELIMS = Tokenizer.INIT_BITSET(EQUAL_CHAR, ',');
|
||||
|
||||
/**
|
||||
* The token parser used to extract values from the header.
|
||||
*/
|
||||
private final Tokenizer tokenParser;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of this parser.
|
||||
*/
|
||||
protected CacheControlHeaderParser() {
|
||||
super();
|
||||
this.tokenParser = Tokenizer.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified header and returns a new {@link CacheControl} instance containing the relevant caching
|
||||
* <p>
|
||||
* directives.
|
||||
*
|
||||
* <p>The returned {@link CacheControl} instance will contain the values for "max-age" and "s-maxage" caching
|
||||
* directives parsed from the input header. If the input header does not contain any caching directives or if the
|
||||
* <p>
|
||||
* directives are malformed, the returned {@link CacheControl} instance will have default values for "max-age" and
|
||||
* <p>
|
||||
* "s-maxage" (-1).</p>
|
||||
*
|
||||
* @param header 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");
|
||||
|
||||
long maxAge = -1;
|
||||
long sharedMaxAge = -1;
|
||||
|
||||
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(maxAge, sharedMaxAge);
|
||||
}
|
||||
buffer = new CharArrayBuffer(s.length());
|
||||
buffer.append(s);
|
||||
cursor = new Tokenizer.Cursor(0, buffer.length());
|
||||
}
|
||||
|
||||
while (!cursor.atEnd()) {
|
||||
final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
|
||||
if (cursor.atEnd()) {
|
||||
return new CacheControl(maxAge, sharedMaxAge);
|
||||
}
|
||||
final int valueDelim = buffer.charAt(cursor.getPos());
|
||||
cursor.updatePos(cursor.getPos() + 1);
|
||||
if (valueDelim != EQUAL_CHAR) {
|
||||
continue;
|
||||
}
|
||||
final String value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
|
||||
|
||||
if (!cursor.atEnd()) {
|
||||
cursor.updatePos(cursor.getPos() + 1);
|
||||
}
|
||||
|
||||
try {
|
||||
if (name.equalsIgnoreCase("s-maxage")) {
|
||||
sharedMaxAge = Long.parseLong(value);
|
||||
} else if (name.equalsIgnoreCase("max-age")) {
|
||||
maxAge = Long.parseLong(value);
|
||||
}
|
||||
} catch (final NumberFormatException e) {
|
||||
// skip malformed directive
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Header {} was malformed: {}", name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CacheControl(maxAge, sharedMaxAge);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +26,9 @@
|
|||
*/
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
@ -50,6 +52,25 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
class ResponseCachingPolicy {
|
||||
|
||||
/**
|
||||
* The default freshness duration for a cached object, in seconds.
|
||||
*
|
||||
* <p>This constant is used to set the default value for the freshness lifetime of a cached object.
|
||||
* When a new object is added to the cache, it will be assigned this duration if no other duration
|
||||
* is specified.</p>
|
||||
*
|
||||
* <p>By default, this value is set to 300 seconds (5 minutes). Applications can customize this
|
||||
* value as needed.</p>
|
||||
*/
|
||||
private static final Duration DEFAULT_FRESHNESS_DURATION = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* This {@link DateTimeFormatter} is used to format and parse date-time objects in a specific format commonly
|
||||
* used in HTTP protocol messages. The format includes the day of the week, day of the month, month, year, and time
|
||||
* of day, all represented in GMT time. An example of a date-time string in this format is "Tue, 15 Nov 1994 08:12:31 GMT".
|
||||
*/
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
|
||||
private static final String[] AUTH_CACHEABLE_PARAMS = {
|
||||
"s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC
|
||||
};
|
||||
|
@ -108,7 +129,8 @@ class ResponseCachingPolicy {
|
|||
public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) {
|
||||
boolean cacheable = false;
|
||||
|
||||
if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)) {
|
||||
if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)
|
||||
&& !HeaderConstants.POST_METHOD.equals(httpMethod)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("{} method response is not cacheable", httpMethod);
|
||||
}
|
||||
|
@ -181,6 +203,15 @@ class ResponseCachingPolicy {
|
|||
return false;
|
||||
}
|
||||
|
||||
// calculate freshness lifetime
|
||||
final Duration freshnessLifetime = calculateFreshnessLifetime(response);
|
||||
if (freshnessLifetime.isNegative() || freshnessLifetime.isZero()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Freshness lifetime is invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return cacheable || isExplicitlyCacheable(response);
|
||||
}
|
||||
|
||||
|
@ -320,4 +351,74 @@ class ResponseCachingPolicy {
|
|||
return HttpVersion.HTTP_1_0.equals(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the freshness lifetime of a response, based on the headers in the response.
|
||||
* <p>
|
||||
* This method follows the algorithm for calculating the freshness lifetime.
|
||||
* The freshness lifetime represents the time interval in seconds during which the response can be served without
|
||||
* being considered stale. The freshness lifetime calculation takes into account the s-maxage, max-age, Expires, and
|
||||
* Date headers as follows:
|
||||
* <ul>
|
||||
* <li>If the s-maxage directive is present in the Cache-Control header of the response, its value is used as the
|
||||
* freshness lifetime for shared caches, which typically serve multiple users or clients.</li>
|
||||
* <li>If the max-age directive is present in the Cache-Control header of the response, its value is used as the
|
||||
* freshness lifetime for private caches, which serve a single user or client.</li>
|
||||
* <li>If the Expires header is present in the response, its value is used as the expiration time of the response.
|
||||
* The freshness lifetime is calculated as the difference between the expiration time and the time specified in the
|
||||
* Date header of the response.</li>
|
||||
* <li>If none of the above headers are present or if the calculated freshness lifetime is invalid, a default value of
|
||||
* 5 minutes is returned.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Note that caching is a complex topic and cache control directives may interact with each other in non-trivial ways.
|
||||
* This method provides a basic implementation of the freshness lifetime calculation algorithm and may not be suitable
|
||||
* for all use cases. Developers should consult the HTTP caching specifications for more information and consider
|
||||
* implementing additional caching mechanisms as needed.
|
||||
* </p>
|
||||
*
|
||||
* @param response the HTTP response for which to calculate the freshness lifetime
|
||||
* @return the freshness lifetime of the response, in seconds
|
||||
*/
|
||||
private Duration calculateFreshnessLifetime(final HttpResponse response) {
|
||||
// Check if s-maxage is present and use its value if it is
|
||||
final Header cacheControl = response.getFirstHeader(HttpHeaders.CACHE_CONTROL);
|
||||
if (cacheControl == null) {
|
||||
// If no cache-control header is present, assume no caching directives and return a default value
|
||||
return DEFAULT_FRESHNESS_DURATION; // 5 minutes
|
||||
}
|
||||
|
||||
final String cacheControlValue = cacheControl.getValue();
|
||||
if (cacheControlValue == null) {
|
||||
// If cache-control header has no value, assume no caching directives and return a default value
|
||||
return DEFAULT_FRESHNESS_DURATION; // 5 minutes
|
||||
}
|
||||
final CacheControl cacheControlHeader = CacheControlHeaderParser.INSTANCE.parse(cacheControl);
|
||||
if (cacheControlHeader.getSharedMaxAge() != -1) {
|
||||
return Duration.ofSeconds(cacheControlHeader.getSharedMaxAge());
|
||||
} else if (cacheControlHeader.getMaxAge() != -1) {
|
||||
return Duration.ofSeconds(cacheControlHeader.getMaxAge());
|
||||
}
|
||||
|
||||
// Check if Expires is present and use its value minus the value of the Date header
|
||||
Instant expiresInstant = null;
|
||||
Instant dateInstant = null;
|
||||
final Header expire = response.getFirstHeader(HttpHeaders.EXPIRES);
|
||||
if (expire != null) {
|
||||
final String expiresHeaderValue = expire.getValue();
|
||||
expiresInstant = FORMATTER.parse(expiresHeaderValue, Instant::from);
|
||||
}
|
||||
final Header date = response.getFirstHeader(HttpHeaders.DATE);
|
||||
if (date != null) {
|
||||
final String dateHeaderValue = date.getValue();
|
||||
dateInstant = FORMATTER.parse(dateHeaderValue, Instant::from);
|
||||
}
|
||||
if (expiresInstant != null && dateInstant != null) {
|
||||
return Duration.ofSeconds(expiresInstant.getEpochSecond() - dateInstant.getEpochSecond());
|
||||
}
|
||||
|
||||
// If none of the above conditions are met, a heuristic freshness lifetime might be applicable
|
||||
return DEFAULT_FRESHNESS_DURATION; // 5 minutes
|
||||
}
|
||||
|
||||
}
|
||||
|
|
109
httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java
vendored
Normal file
109
httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* ====================================================================
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
* ====================================================================
|
||||
*
|
||||
* This software consists of voluntary contributions made by many
|
||||
* individuals on behalf of the Apache Software Foundation. For more
|
||||
* information on the Apache Software Foundation, please see
|
||||
* <http://www.apache.org/>.
|
||||
*
|
||||
*/
|
||||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.message.BasicHeader;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class CacheControlParserTest {
|
||||
|
||||
private final CacheControlHeaderParser parser = CacheControlHeaderParser.INSTANCE;
|
||||
|
||||
@Test
|
||||
public void testParseMaxAgeZero() {
|
||||
final Header header = new BasicHeader("Cache-Control", "max-age=0 , this = stuff;");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(0L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSMaxAge() {
|
||||
final Header header = new BasicHeader("Cache-Control", "s-maxage=3600");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(3600L, cacheControl.getSharedMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseInvalidCacheValue() {
|
||||
final Header header = new BasicHeader("Cache-Control", "max-age=invalid");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseInvalidHeader() {
|
||||
final Header header = new BasicHeader("Cache-Control", "max-age");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNullHeader() {
|
||||
final Header header = null;
|
||||
assertThrows(NullPointerException.class, () -> parser.parse(header));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseEmptyHeader() {
|
||||
final Header header = new BasicHeader("Cache-Control", "");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCookieEmptyValue() {
|
||||
final Header header = new BasicHeader("Cache-Control", "max-age=;");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNoCache() {
|
||||
final Header header = new BasicHeader(" Cache-Control", "no-cache");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseNoDirective() {
|
||||
final Header header = new BasicHeader(" Cache-Control", "");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGarbage() {
|
||||
final Header header = new BasicHeader("Cache-Control", ",,= blah,");
|
||||
final CacheControl cacheControl = parser.parse(header);
|
||||
assertEquals(-1L, cacheControl.getMaxAge());
|
||||
}
|
||||
|
||||
}
|
|
@ -1165,12 +1165,12 @@ public class TestCachingExecChain {
|
|||
|
||||
final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
|
||||
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
|
||||
resp1.setHeader("Cache-Control", "max-age=0");
|
||||
resp1.setHeader("Cache-Control", "max-age=1");
|
||||
resp1.setHeader("Etag", "etag");
|
||||
|
||||
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
|
||||
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
|
||||
resp2.setHeader("Cache-Control", "max-age=0");
|
||||
resp2.setHeader("Cache-Control", "max-age=1");
|
||||
resp1.setHeader("Etag", "etag");
|
||||
|
||||
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
|
||||
|
@ -1199,13 +1199,13 @@ public class TestCachingExecChain {
|
|||
|
||||
final ClassicHttpResponse resp1 = HttpTestUtils.make304Response();
|
||||
resp1.setHeader("Date", DateUtils.formatStandardDate(now));
|
||||
resp1.setHeader("Cache-Control", "max-age=0");
|
||||
resp1.setHeader("Cache-Control", "max-age=1");
|
||||
resp1.setHeader("Etag", "etag");
|
||||
resp1.setHeader("Vary", "Accept-Encoding");
|
||||
|
||||
final ClassicHttpResponse resp2 = HttpTestUtils.make304Response();
|
||||
resp2.setHeader("Date", DateUtils.formatStandardDate(now));
|
||||
resp2.setHeader("Cache-Control", "max-age=0");
|
||||
resp2.setHeader("Cache-Control", "max-age=1");
|
||||
resp1.setHeader("Etag", "etag");
|
||||
resp1.setHeader("Vary", "Accept-Encoding");
|
||||
|
||||
|
|
|
@ -27,11 +27,13 @@
|
|||
package org.apache.hc.client5.http.impl.cache;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.hc.client5.http.auth.StandardAuthScheme;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpOptions;
|
||||
import org.apache.hc.client5.http.utils.DateUtils;
|
||||
import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
|
@ -216,15 +218,49 @@ public class TestResponseCachingPolicy {
|
|||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.setHeader("Cache-Control", "max-age=0");
|
||||
Assertions.assertFalse(policy.isResponseCacheable("GET", response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedHeaderValue() {
|
||||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.setHeader("Cache-Control", "max-age=boom");
|
||||
Assertions.assertTrue(policy.isResponseCacheable("GET", response));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNullHeaderValue() {
|
||||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.setHeader("Cache-Control", null);
|
||||
Assertions.assertTrue(policy.isResponseCacheable("GET", response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleHeaderValue() {
|
||||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.setHeader("Cache-Control", "s-maxage=12,must-revalidate=true,max-age=23");
|
||||
Assertions.assertTrue(policy.isResponseCacheable("GET", response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingCacheControlHeader() {
|
||||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.removeHeaders(HttpHeaders.CACHE_CONTROL);
|
||||
final boolean isCacheable = policy.isResponseCacheable("GET", response);
|
||||
Assertions.assertTrue(isCacheable);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNon206WithSMaxAgeIsCacheable() {
|
||||
final int status = getRandomStatus();
|
||||
response.setCode(status);
|
||||
response.setHeader("Cache-Control", "s-maxage=0");
|
||||
Assertions.assertTrue(policy.isResponseCacheable("GET", response));
|
||||
Assertions.assertFalse(policy.isResponseCacheable("GET", response));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -870,9 +906,99 @@ public class TestResponseCachingPolicy {
|
|||
response.setCode(HttpStatus.SC_NOT_FOUND);
|
||||
response.setHeader("Date", DateUtils.formatStandardDate(now));
|
||||
response.setHeader("Cache-Control","max-age=300");
|
||||
Assertions.assertTrue(policy.isResponseCacheable(request, response));
|
||||
assertTrue(policy.isResponseCacheable(request, response));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsResponseCacheableNullCacheControl() {
|
||||
|
||||
// Set up test data
|
||||
final long now = System.currentTimeMillis();
|
||||
final long tenSecondsFromNow = now + 10000;
|
||||
|
||||
response = new BasicHttpResponse(HttpStatus.SC_OK, "");
|
||||
response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123));
|
||||
response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123));
|
||||
|
||||
|
||||
// Create ResponseCachingPolicy instance and test the method
|
||||
policy = new ResponseCachingPolicy(0, true, false, false, false);
|
||||
request = new BasicHttpRequest("POST", "/foo");
|
||||
final boolean isCacheable = policy.isResponseCacheable(request, response);
|
||||
|
||||
// Verify the result
|
||||
assertTrue(isCacheable);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testIsResponseCacheableNotNullCacheControlSmaxAge60() {
|
||||
|
||||
// Set up test data
|
||||
final long now = System.currentTimeMillis();
|
||||
final long tenSecondsFromNow = now + 10000;
|
||||
|
||||
response = new BasicHttpResponse(HttpStatus.SC_OK, "");
|
||||
response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123));
|
||||
response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123));
|
||||
|
||||
|
||||
// Create ResponseCachingPolicy instance and test the method
|
||||
policy = new ResponseCachingPolicy(0, true, false, false, false);
|
||||
request = new BasicHttpRequest("POST", "/foo");
|
||||
response.addHeader(HttpHeaders.CACHE_CONTROL, "s-maxage=60");
|
||||
final boolean isCacheable = policy.isResponseCacheable(request, response);
|
||||
|
||||
// Verify the result
|
||||
assertTrue(isCacheable);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsResponseCacheableNotNullCacheControlMaxAge60() {
|
||||
|
||||
// Set up test data
|
||||
final long now = System.currentTimeMillis();
|
||||
final long tenSecondsFromNow = now + 10000;
|
||||
|
||||
response = new BasicHttpResponse(HttpStatus.SC_OK, "");
|
||||
response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123));
|
||||
response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123));
|
||||
|
||||
|
||||
// Create ResponseCachingPolicy instance and test the method
|
||||
policy = new ResponseCachingPolicy(0, true, false, false,false);
|
||||
request = new BasicHttpRequest("POST", "/foo");
|
||||
response.addHeader(HttpHeaders.CACHE_CONTROL, "max-age=60");
|
||||
final boolean isCacheable = policy.isResponseCacheable(request, response);
|
||||
|
||||
// Verify the result
|
||||
assertTrue(isCacheable);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsResponseCacheableNotExsiresAndDate() {
|
||||
|
||||
// Set up test data
|
||||
final long now = System.currentTimeMillis();
|
||||
final long tenSecondsFromNow = now + 10000;
|
||||
|
||||
response = new BasicHttpResponse(HttpStatus.SC_OK, "");
|
||||
response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123));
|
||||
response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123));
|
||||
|
||||
|
||||
// Create ResponseCachingPolicy instance and test the method
|
||||
policy = new ResponseCachingPolicy(0, true, false, false,false);
|
||||
request = new BasicHttpRequest("POST", "/foo");
|
||||
response.addHeader(HttpHeaders.CACHE_CONTROL, "something");
|
||||
final boolean isCacheable = policy.isResponseCacheable(request, response);
|
||||
|
||||
// Verify the result
|
||||
assertTrue(isCacheable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private int getRandomStatus() {
|
||||
final int rnd = new Random().nextInt(acceptableCodes.length);
|
||||
|
||||
|
|
Loading…
Reference in New Issue