From ff3569cd5ec2b06c5a54a74f089c29bdd53ce0cc Mon Sep 17 00:00:00 2001
From: Oleg Kalnichevski
Date: Sun, 3 Sep 2023 11:10:48 +0200
Subject: [PATCH 1/3] HTTPCLIENT-2277: Deprecated 303 response caching switch
as no longer required by RFC 9111
---
.../client5/http/impl/cache/CacheConfig.java | 32 +++------
.../http/impl/cache/CachingExecBase.java | 1 -
.../impl/cache/ResponseCachingPolicy.java | 66 +++++--------------
.../impl/cache/TestResponseCachingPolicy.java | 15 +----
4 files changed, 26 insertions(+), 88 deletions(-)
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
index cde5b7077..6916e5e0c 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
@@ -57,15 +57,6 @@
* browser cache), then you will want to {@link
* CacheConfig#isSharedCache()} turn off the shared cache setting}.
*
- * 303 caching. RFC2616 explicitly disallows caching 303 responses;
- * however, the HTTPbis working group says they can be cached
- * if explicitly indicated in the response headers and permitted by the request method.
- * (They also indicate that disallowing 303 caching is actually an unintended
- * spec error in RFC2616).
- * This behavior is off by default, to err on the side of a conservative
- * adherence to the existing standard, but you may want to
- * {@link Builder#setAllow303Caching(boolean) enable it}.
- *
*
Weak ETags on PUT/DELETE If-Match requests. RFC2616 explicitly
* prohibits the use of weak validators in non-GET requests, however, the
* HTTPbis working group says while the limitation for weak validators on ranged
@@ -113,8 +104,10 @@ public class CacheConfig implements Cloneable {
*/
public final static int DEFAULT_MAX_UPDATE_RETRIES = 1;
- /** Default setting for 303 caching
+ /**
+ * @deprecated No longer applicable. Do not use.
*/
+ @Deprecated
public final static boolean DEFAULT_303_CACHING_ENABLED = false;
/**
@@ -147,7 +140,6 @@ public class CacheConfig implements Cloneable {
private final long maxObjectSize;
private final int maxCacheEntries;
private final int maxUpdateRetries;
- private final boolean allow303Caching;
private final boolean heuristicCachingEnabled;
private final float heuristicCoefficient;
private final TimeValue heuristicDefaultLifetime;
@@ -168,7 +160,6 @@ public class CacheConfig implements Cloneable {
final long maxObjectSize,
final int maxCacheEntries,
final int maxUpdateRetries,
- final boolean allow303Caching,
final boolean heuristicCachingEnabled,
final float heuristicCoefficient,
final TimeValue heuristicDefaultLifetime,
@@ -182,7 +173,6 @@ public class CacheConfig implements Cloneable {
this.maxObjectSize = maxObjectSize;
this.maxCacheEntries = maxCacheEntries;
this.maxUpdateRetries = maxUpdateRetries;
- this.allow303Caching = allow303Caching;
this.heuristicCachingEnabled = heuristicCachingEnabled;
this.heuristicCoefficient = heuristicCoefficient;
this.heuristicDefaultLifetime = heuristicDefaultLifetime;
@@ -257,11 +247,11 @@ public int getMaxUpdateRetries(){
}
/**
- * Returns whether 303 caching is enabled.
- * @return {@code true} if it is enabled.
+ * @deprecated No longer applicable. Do not use.
*/
+ @Deprecated
public boolean is303CachingEnabled() {
- return allow303Caching;
+ return true;
}
/**
@@ -356,7 +346,6 @@ public static class Builder {
private long maxObjectSize;
private int maxCacheEntries;
private int maxUpdateRetries;
- private boolean allow303Caching;
private boolean heuristicCachingEnabled;
private float heuristicCoefficient;
private TimeValue heuristicDefaultLifetime;
@@ -371,7 +360,6 @@ public static class Builder {
this.maxObjectSize = DEFAULT_MAX_OBJECT_SIZE_BYTES;
this.maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES;
this.maxUpdateRetries = DEFAULT_MAX_UPDATE_RETRIES;
- this.allow303Caching = DEFAULT_303_CACHING_ENABLED;
this.heuristicCachingEnabled = DEFAULT_HEURISTIC_CACHING_ENABLED;
this.heuristicCoefficient = DEFAULT_HEURISTIC_COEFFICIENT;
this.heuristicDefaultLifetime = DEFAULT_HEURISTIC_LIFETIME;
@@ -407,12 +395,10 @@ public Builder setMaxUpdateRetries(final int maxUpdateRetries) {
}
/**
- * Enables or disables 303 caching.
- * @param allow303Caching should be {@code true} to
- * permit 303 caching, {@code false} to disable it.
+ * @deprecated Has no effect. Do not use.
*/
+ @Deprecated
public Builder setAllow303Caching(final boolean allow303Caching) {
- this.allow303Caching = allow303Caching;
return this;
}
@@ -537,7 +523,6 @@ public CacheConfig build() {
maxObjectSize,
maxCacheEntries,
maxUpdateRetries,
- allow303Caching,
heuristicCachingEnabled,
heuristicCoefficient,
heuristicDefaultLifetime,
@@ -557,7 +542,6 @@ public String toString() {
builder.append("[maxObjectSize=").append(this.maxObjectSize)
.append(", maxCacheEntries=").append(this.maxCacheEntries)
.append(", maxUpdateRetries=").append(this.maxUpdateRetries)
- .append(", 303CachingEnabled=").append(this.allow303Caching)
.append(", heuristicCachingEnabled=").append(this.heuristicCachingEnabled)
.append(", heuristicCoefficient=").append(this.heuristicCoefficient)
.append(", heuristicDefaultLifetime=").append(this.heuristicDefaultLifetime)
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
index e9c781fa8..e4fc8226b 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
@@ -98,7 +98,6 @@ public class CachingExecBase {
this.cacheConfig.getMaxObjectSize(),
this.cacheConfig.isSharedCache(),
this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(),
- this.cacheConfig.is303CachingEnabled(),
this.cacheConfig.isNeverCacheHTTP11ResponsesWithQuery(),
this.cacheConfig.isStaleIfErrorEnabled());
}
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 521683af6..74239cf00 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
@@ -29,9 +29,6 @@
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;
import java.util.Iterator;
import java.util.Set;
@@ -71,20 +68,12 @@ class ResponseCachingPolicy {
*/
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME;
- private final static Set CACHEABLE_STATUS_CODES =
- new HashSet<>(Arrays.asList(HttpStatus.SC_OK,
- HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION,
- HttpStatus.SC_MULTIPLE_CHOICES,
- HttpStatus.SC_MOVED_PERMANENTLY,
- HttpStatus.SC_GONE));
-
private static final Logger LOG = LoggerFactory.getLogger(ResponseCachingPolicy.class);
private final long maxObjectSizeBytes;
private final boolean sharedCache;
private final boolean neverCache1_0ResponsesWithQueryString;
private final boolean neverCache1_1ResponsesWithQueryString;
- private final Set uncacheableStatusCodes;
/**
* A flag indicating whether serving stale cache entries is allowed when an error occurs
@@ -94,32 +83,6 @@ class ResponseCachingPolicy {
*/
private final boolean staleIfErrorEnabled;
- /**
- * Define a cache policy that limits the size of things that should be stored
- * in the cache to a maximum of {@link HttpResponse} bytes in size.
- *
- * @param maxObjectSizeBytes the size to limit items into the cache
- * @param sharedCache whether to behave as a shared cache (true) or a
- * non-shared/private cache (false)
- * @param neverCache1_0ResponsesWithQueryString true to never cache HTTP 1.0 responses with a query string, false
- * to cache if explicit cache headers are found.
- * @param allow303Caching if this policy is permitted to cache 303 response
- * @param neverCache1_1ResponsesWithQueryString {@code true} to never cache HTTP 1.1 responses with a query string,
- * {@code false} to cache if explicit cache headers are found.
- */
- public ResponseCachingPolicy(final long maxObjectSizeBytes,
- final boolean sharedCache,
- final boolean neverCache1_0ResponsesWithQueryString,
- final boolean allow303Caching,
- final boolean neverCache1_1ResponsesWithQueryString) {
- this(maxObjectSizeBytes,
- sharedCache,
- neverCache1_0ResponsesWithQueryString,
- allow303Caching,
- neverCache1_1ResponsesWithQueryString,
- false);
- }
-
/**
* Constructs a new ResponseCachingPolicy with the specified cache policy settings and stale-if-error support.
*
@@ -129,8 +92,6 @@ public ResponseCachingPolicy(final long maxObjectSizeBytes,
* non-shared/private cache (false)
* @param neverCache1_0ResponsesWithQueryString {@code true} to never cache HTTP 1.0 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
- * @param allow303Caching {@code true} if this policy is permitted to cache 303 responses,
- * {@code false} otherwise
* @param neverCache1_1ResponsesWithQueryString {@code true} to never cache HTTP 1.1 responses with a query string,
* {@code false} to cache if explicit cache headers are found.
* @param staleIfErrorEnabled {@code true} to enable the stale-if-error cache directive, which
@@ -141,18 +102,12 @@ public ResponseCachingPolicy(final long maxObjectSizeBytes,
public ResponseCachingPolicy(final long maxObjectSizeBytes,
final boolean sharedCache,
final boolean neverCache1_0ResponsesWithQueryString,
- final boolean allow303Caching,
final boolean neverCache1_1ResponsesWithQueryString,
final boolean staleIfErrorEnabled) {
this.maxObjectSizeBytes = maxObjectSizeBytes;
this.sharedCache = sharedCache;
this.neverCache1_0ResponsesWithQueryString = neverCache1_0ResponsesWithQueryString;
this.neverCache1_1ResponsesWithQueryString = neverCache1_1ResponsesWithQueryString;
- if (allow303Caching) {
- uncacheableStatusCodes = new HashSet<>(Collections.singletonList(HttpStatus.SC_PARTIAL_CONTENT));
- } else {
- uncacheableStatusCodes = new HashSet<>(Arrays.asList(HttpStatus.SC_PARTIAL_CONTENT, HttpStatus.SC_SEE_OTHER));
- }
this.staleIfErrorEnabled = staleIfErrorEnabled;
}
@@ -172,15 +127,15 @@ public boolean isResponseCacheable(final ResponseCacheControl cacheControl, fina
}
final int status = response.getCode();
- if (CACHEABLE_STATUS_CODES.contains(status)) {
+ if (isKnownCacheableStatusCode(status)) {
// these response codes MAY be cached
cacheable = true;
- } else if (uncacheableStatusCodes.contains(status)) {
+ } else if (isKnownNonCacheableStatusCode(status)) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} response is not cacheable", status);
}
return false;
- } else if (unknownStatusCode(status)) {
+ } else if (isUnknownStatusCode(status)) {
// a response with an unknown status code MUST NOT be
// cached
if (LOG.isDebugEnabled()) {
@@ -247,7 +202,19 @@ public boolean isResponseCacheable(final ResponseCacheControl cacheControl, fina
return cacheable || isExplicitlyCacheable(cacheControl, response);
}
- private boolean unknownStatusCode(final int status) {
+ private static boolean isKnownCacheableStatusCode(final int status) {
+ return status == HttpStatus.SC_OK ||
+ status == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION ||
+ status == HttpStatus.SC_MULTIPLE_CHOICES ||
+ status == HttpStatus.SC_MOVED_PERMANENTLY ||
+ status == HttpStatus.SC_GONE;
+ }
+
+ private static boolean isKnownNonCacheableStatusCode(final int status) {
+ return status == HttpStatus.SC_PARTIAL_CONTENT;
+ }
+
+ private static boolean isUnknownStatusCode(final int status) {
if (status >= 100 && status <= 101) {
return false;
}
@@ -507,7 +474,6 @@ boolean responseContainsNoCacheDirective(final ResponseCacheControl responseCach
}
/**
- * This method checks if a given HTTP status code is understood according to RFC 7231.
* Understood status codes include:
* - All 2xx (Successful) status codes (200-299)
* - All 3xx (Redirection) status codes (300-399)
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 3c148a380..464166d4c 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
@@ -909,17 +909,6 @@ public void test302WithExplicitCachingHeaders() {
Assertions.assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
}
- @Test
- public void test303WithExplicitCachingHeadersUnderDefaultBehavior() {
- // RFC 2616 says: 303 should not be cached
- response.setCode(HttpStatus.SC_SEE_OTHER);
- response.setHeader("Date", DateUtils.formatStandardDate(now));
- responseCacheControl = ResponseCacheControl.builder()
- .setMaxAge(300)
- .build();
- Assertions.assertFalse(policy.isResponseCacheable(responseCacheControl, request, response));
- }
-
@Test
public void test303WithExplicitCachingHeadersWhenPermittedByConfig() {
// HTTPbis working group says ok if explicitly indicated by
@@ -1039,7 +1028,7 @@ public void testIsResponseCacheable() {
request = new BasicHttpRequest("GET","/foo?s=bar");
// HTTPbis working group says ok if explicitly indicated by
// response headers
- policy = new ResponseCachingPolicy(0, true, false, false, true);
+ policy = new ResponseCachingPolicy(0, true, false, true, true);
response.setCode(HttpStatus.SC_OK);
response.setHeader("Date", DateUtils.formatStandardDate(now));
assertTrue(policy.isResponseCacheable(responseCacheControl, request, response));
@@ -1052,7 +1041,7 @@ void testIsResponseCacheableNoCache() {
response.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
// Create ResponseCachingPolicy instance and test the method
- policy = new ResponseCachingPolicy(0, true, false, false, false);
+ policy = new ResponseCachingPolicy(0, true, false, false, true);
request = new BasicHttpRequest("GET", "/foo");
responseCacheControl = ResponseCacheControl.builder()
.setNoCache(true)
From 926b642abc3e44030711365f11577eb949e35847 Mon Sep 17 00:00:00 2001
From: Oleg Kalnichevski
Date: Tue, 5 Sep 2023 09:50:12 +0200
Subject: [PATCH 2/3] Javadoc improvements (no functional changes)
---
.../client5/http/impl/cache/CacheConfig.java | 59 +++++++------------
1 file changed, 22 insertions(+), 37 deletions(-)
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
index 6916e5e0c..c1bfc61e0 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheConfig.java
@@ -30,21 +30,18 @@
import org.apache.hc.core5.util.TimeValue;
/**
- * Java Beans-style configuration for caching {@link org.apache.hc.client5.http.classic.HttpClient}.
- * Any class in the caching module that has configuration options should take a
- * {@link CacheConfig} argument in one of its constructors. A
- * {@code CacheConfig} instance has sane and conservative defaults, so the
- * easiest way to specify options is to get an instance and then set just
- * the options you want to modify from their defaults.
- *
- * N.B. This class is only for caching-specific configuration; to
- * configure the behavior of the rest of the client, configure the
- * {@link org.apache.hc.client5.http.classic.HttpClient} used as the "backend"
- * for the {@code CachingHttpClient}.
+ * Configuration for HTTP caches
*
* Cache configuration can be grouped into the following categories:
*
- * Cache size. If the backend storage supports these limits, you
+ *
Protocol options. I some cases the HTTP protocol allows for
+ * conditional behaviors or optional protocol extensions. Such conditional
+ * protocol behaviors or extensions can be turned on or off here.
+ * See {@link CacheConfig#isNeverCacheHTTP10ResponsesWithQuery()},
+ * {@link CacheConfig#isNeverCacheHTTP11ResponsesWithQuery()},
+ * {@link CacheConfig#isStaleIfErrorEnabled()}
+ *
+ * Cache size. If the backend storage supports these limits, one
* can specify the {@link CacheConfig#getMaxCacheEntries maximum number of
* cache entries} as well as the {@link CacheConfig#getMaxObjectSize()}
* maximum cacheable response body size}.
@@ -54,37 +51,25 @@
* responses to requests with {@code Authorization} headers or responses
* marked with {@code Cache-Control: private}. If, however, the cache
* is only going to be used by one logical "user" (behaving similarly to a
- * browser cache), then you will want to {@link
- * CacheConfig#isSharedCache()} turn off the shared cache setting}.
- *
- * Weak ETags on PUT/DELETE If-Match requests. RFC2616 explicitly
- * prohibits the use of weak validators in non-GET requests, however, the
- * HTTPbis working group says while the limitation for weak validators on ranged
- * requests makes sense, weak ETag validation is useful on full non-GET
- * requests; e.g., PUT with If-Match. This behavior is off by default, to err on
- * the side of a conservative adherence to the existing standard, but you may
- * want to {@link Builder#setWeakETagOnPutDeleteAllowed(boolean) enable it}.
+ * browser cache), then one may want to {@link CacheConfig#isSharedCache()}
+ * turn off the shared cache setting}.
*
- * Heuristic caching. Per RFC2616, a cache may cache certain cache
- * entries even if no explicit cache control headers are set by the origin.
- * This behavior is off by default, but you may want to turn this on if you
- * are working with an origin that doesn't set proper headers but where you
- * still want to cache the responses. You will want to {@link
- * CacheConfig#isHeuristicCachingEnabled()} enable heuristic caching},
+ *
Heuristic caching. Per HTTP caching specification, a cache may
+ * cache certain cache entries even if no explicit cache control headers are
+ * set by the origin. This behavior is off by default, but you may want to
+ * turn this on if you are working with an origin that doesn't set proper
+ * headers but where one may still want to cache the responses. Use {@link
+ * CacheConfig#isHeuristicCachingEnabled()} to enable heuristic caching},
* then specify either a {@link CacheConfig#getHeuristicDefaultLifetime()
* default freshness lifetime} and/or a {@link
* CacheConfig#getHeuristicCoefficient() fraction of the time since
- * the resource was last modified}. See Sections
- *
- * 13.2.2 and
- * 13.2.4 of the HTTP/1.1 RFC for more details on heuristic caching.
+ * the resource was last modified}.
*
* Background validation. The cache module supports the
- * {@code stale-while-revalidate} directive of
- * RFC5861, which allows
- * certain cache entry revalidations to happen in the background. Asynchronous
- * validation is enabled by default but it could be disabled by setting the number
- * of re-validation workers to {@code 0} with {@link CacheConfig#getAsynchronousWorkers()}
+ * {@code stale-while-revalidate} directive, which allows certain cache entry
+ * revalidations to happen in the background. Asynchronous validation is enabled
+ * by default but it could be disabled by setting the number of re-validation
+ * workers to {@code 0} with {@link CacheConfig#getAsynchronousWorkers()}
* parameter
*/
public class CacheConfig implements Cloneable {
From 0cc6a6d6ee3702cb35ed4f132a07eb8f87d9715a Mon Sep 17 00:00:00 2001
From: Oleg Kalnichevski
Date: Mon, 11 Sep 2023 12:01:12 +0200
Subject: [PATCH 3/3] HTTPCLIENT-2277: Revision of HTTP cache protocol
requirement and recommendation test cases: * Removed links to RFC 2616 *
Removed verbatim quotes from RFC 2616 * Removed obsolete test cases and test
cases without result verification / assertions * Removed test cases unrelated
to HTTP caching * Removed test cases without test result assertions
---
.../cache/CachedHttpResponseGenerator.java | 3 +-
.../http/impl/cache/CachingExecBase.java | 14 +-
.../http/impl/cache/HttpTestUtils.java | 69 +-
.../impl/cache/TestCacheKeyGenerator.java | 14 -
.../impl/cache/TestProtocolDeviations.java | 185 --
.../cache/TestProtocolRecommendations.java | 320 +--
.../impl/cache/TestProtocolRequirements.java | 2395 ++---------------
7 files changed, 265 insertions(+), 2735 deletions(-)
delete mode 100644 httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolDeviations.java
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java
index 1cd79428a..388387121 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachedHttpResponseGenerator.java
@@ -100,9 +100,8 @@ SimpleHttpResponse generateNotModifiedResponse(final HttpCacheEntry entry) {
final SimpleHttpResponse response = new SimpleHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
// The response MUST include the following headers
- // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
- // - Date, unless its omission is required by section 14.8.1
+ // - Date
Header dateHeader = entry.getFirstHeader(HttpHeaders.DATE);
if (dateHeader == null) {
dateHeader = new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now()));
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
index e4fc8226b..273514aa4 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
@@ -56,8 +56,6 @@
public class CachingExecBase {
- final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
-
final AtomicLong cacheHits = new AtomicLong();
final AtomicLong cacheMisses = new AtomicLong();
final AtomicLong cacheUpdates = new AtomicLong();
@@ -267,16 +265,6 @@ void setResponseStatus(final HttpContext context, final CacheResponseStatus valu
}
}
- /**
- * Reports whether this {@code CachingHttpClient} implementation
- * supports byte-range requests as specified by the {@code Range}
- * and {@code Content-Range} headers.
- * @return {@code true} if byte-range requests are supported
- */
- boolean supportsRangeAndContentRangeHeaders() {
- return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
- }
-
Instant getCurrentDate() {
return Instant.now();
}
@@ -298,7 +286,7 @@ boolean revalidationResponseIsTooOld(final HttpResponse backendResponse, final H
// either backend response or cached entry did not have a valid
// Date header, so we can't tell if they are out of order
// according to the origin clock; thus we can skip the
- // unconditional retry recommended in 13.2.6 of RFC 2616.
+ // unconditional retry.
return DateSupport.isBefore(backendResponse, cacheEntry, HttpHeaders.DATE);
}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java
index c587856ef..7f5a8d056 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java
@@ -30,6 +30,7 @@
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
+import java.util.Iterator;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
@@ -61,24 +62,6 @@
public class HttpTestUtils {
- private static final String[] SINGLE_HEADERS = { "Accept-Ranges", "Age", "Authorization",
- "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type",
- "Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since",
- "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location",
- "Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
- "User-Agent", "Vary" };
-
- /*
- * Determines whether a given header name may only appear once in a message.
- */
- public static boolean isSingleHeader(final String name) {
- for (final String s : SINGLE_HEADERS) {
- if (s.equalsIgnoreCase(name)) {
- return true;
- }
- }
- return false;
- }
/*
* Assertions.asserts that two request or response bodies are byte-equivalent.
*/
@@ -103,24 +86,28 @@ public static boolean equivalent(final HttpEntity e1, final HttpEntity e2) throw
/*
* Retrieves the full header value by combining multiple headers and
* separating with commas, canonicalizing whitespace along the way.
- *
- * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
*/
public static String getCanonicalHeaderValue(final HttpMessage r, final String name) {
- if (isSingleHeader(name)) {
+ final int n = r.countHeaders(name);
+ r.getFirstHeader(name);
+ if (n == 0) {
+ return null;
+ } else if (n == 1) {
final Header h = r.getFirstHeader(name);
- return (h != null) ? h.getValue() : null;
- }
- final StringBuilder buf = new StringBuilder();
- boolean first = true;
- for (final Header h : r.getHeaders(name)) {
- if (!first) {
- buf.append(", ");
+ return h != null ? h.getValue() : null;
+ } else {
+ final StringBuilder buf = new StringBuilder();
+ for (final Iterator it = r.headerIterator(name); it.hasNext(); ) {
+ if (buf.length() > 0) {
+ buf.append(", ");
+ }
+ final Header header = it.next();
+ if (header != null) {
+ buf.append(header.getValue().trim());
+ }
}
- buf.append(h.getValue().trim());
- first = false;
+ return buf.toString();
}
- return buf.toString();
}
/*
@@ -132,7 +119,7 @@ public static boolean isEndToEndHeaderSubset(final HttpMessage r1, final HttpMes
if (!HttpCacheEntryFactory.isHopByHop(h)) {
final String r1val = getCanonicalHeaderValue(r1, h.getName());
final String r2val = getCanonicalHeaderValue(r2, h.getName());
- if (!r1val.equals(r2val)) {
+ if (!Objects.equals(r1val, r2val)) {
return false;
}
}
@@ -146,21 +133,23 @@ public static boolean isEndToEndHeaderSubset(final HttpMessage r1, final HttpMes
* is semantically transparent, the client receives exactly the same
* response (except for hop-by-hop headers) that it would have received had
* its request been handled directly by the origin server."
- *
- * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html#sec1.3
*/
public static boolean semanticallyTransparent(
final ClassicHttpResponse r1, final ClassicHttpResponse r2) throws Exception {
- final boolean entitiesEquivalent = equivalent(r1.getEntity(), r2.getEntity());
- if (!entitiesEquivalent) {
+ final boolean statusLineEquivalent = Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
+ && r1.getCode() == r2.getCode();
+ if (!statusLineEquivalent) {
return false;
}
- final boolean statusLinesEquivalent = Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
- && r1.getCode() == r2.getCode();
- if (!statusLinesEquivalent) {
+ final boolean headerEquivalent = isEndToEndHeaderSubset(r1, r2);
+ if (!headerEquivalent) {
return false;
}
- return isEndToEndHeaderSubset(r1, r2);
+ final boolean entityEquivalent = equivalent(r1.getEntity(), r2.getEntity());
+ if (!entityEquivalent) {
+ return false;
+ }
+ return true;
}
/* Assertions.asserts that protocol versions equivalent. */
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java
index a9c808e3e..b5858961b 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheKeyGenerator.java
@@ -102,20 +102,6 @@ public void testGetURIWithDifferentPortAndScheme() {
new BasicHttpRequest("GET", "/full_episodes")));
}
- /*
- * "When comparing two URIs to decide if they match or not, a client
- * SHOULD use a case-sensitive octet-by-octet comparison of the entire
- * URIs, with these exceptions:
- * - A port that is empty or not given is equivalent to the default
- * port for that URI-reference;
- * - Comparisons of host names MUST be case-insensitive;
- * - Comparisons of scheme names MUST be case-insensitive;
- * - An empty abs_path is equivalent to an abs_path of "/".
- * Characters other than those in the 'reserved' and 'unsafe' sets
- * (see RFC 2396 [42]) are equivalent to their '"%" HEX HEX' encoding."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2.3
- */
@Test
public void testEmptyPortEquivalentToDefaultPortForHttp() {
final HttpHost host1 = new HttpHost("foo.example.com:");
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolDeviations.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolDeviations.java
deleted file mode 100644
index de084a838..000000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolDeviations.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * ====================================================================
- * 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
- * .
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.util.Random;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.cache.HttpCacheContext;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.client5.http.classic.ExecChainHandler;
-import org.apache.hc.client5.http.classic.ExecRuntime;
-import org.apache.hc.client5.http.utils.DateUtils;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.HttpEntity;
-import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
-import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
-import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-/**
- * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
- * of the rules for proxies apply to us, as far as proper operation of the
- * requests that pass through us. Generally speaking, we want to make sure that
- * any response returned from our HttpClient.execute() methods is conditionally
- * compliant with the rules for an HTTP/1.1 server, and that any requests we
- * pass downstream to the backend HttpClient are are conditionally compliant
- * with the rules for an HTTP/1.1 client.
- *
- * There are some cases where strictly behaving as a compliant caching proxy
- * would result in strange behavior, since we're attached as part of a client
- * and are expected to be a drop-in replacement. The test cases captured here
- * document the places where we differ from the HTTP RFC.
- */
-@SuppressWarnings("boxing") // test code
-public class TestProtocolDeviations {
-
- private static final int MAX_BYTES = 1024;
- private static final int MAX_ENTRIES = 100;
-
- HttpHost host;
- HttpRoute route;
- @Mock
- ExecRuntime mockEndpoint;
- @Mock
- ExecChain mockExecChain;
- ClassicHttpRequest request;
- HttpCacheContext context;
- ClassicHttpResponse originResponse;
-
- ExecChainHandler impl;
-
- @BeforeEach
- public void setUp() {
- MockitoAnnotations.openMocks(this);
- host = new HttpHost("foo.example.com", 80);
-
- route = new HttpRoute(host);
-
- request = new BasicClassicHttpRequest("GET", "/foo");
-
- context = HttpCacheContext.create();
-
- originResponse = make200Response();
-
- final CacheConfig config = CacheConfig.custom()
- .setMaxCacheEntries(MAX_ENTRIES)
- .setMaxObjectSize(MAX_BYTES)
- .build();
-
- final HttpCache cache = new BasicHttpCache(config);
- impl = createCachingExecChain(cache, config);
- }
-
- private ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
- return impl.execute(request,
- new ExecChain.Scope("test", route, request, mockEndpoint, context),
- mockExecChain);
- }
-
- protected ExecChainHandler createCachingExecChain(final HttpCache cache, final CacheConfig config) {
- return new CachingExec(cache, null, config);
- }
-
- private ClassicHttpResponse make200Response() {
- final ClassicHttpResponse out = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- out.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- out.setHeader("Server", "MockOrigin/1.0");
- out.setEntity(makeBody(128));
- return out;
- }
-
- private HttpEntity makeBody(final int nbytes) {
- final byte[] bytes = new byte[nbytes];
- new Random().nextBytes(bytes);
- return new ByteArrayEntity(bytes, null);
- }
-
- /*
- * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
- * header field (section 14.47) containing a challenge applicable to the
- * requested resource."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
- */
- @Test
- public void testPassesOnOrigin401ResponseWithoutWWWAuthenticateHeader() throws Exception {
-
- originResponse = new BasicClassicHttpResponse(401, "Unauthorized");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final HttpResponse result = execute(request);
- Assertions.assertSame(originResponse, result);
- }
-
- /*
- * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
- * header containing a list of valid methods for the requested resource.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
- */
- @Test
- public void testPassesOnOrigin405WithoutAllowHeader() throws Exception {
- originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final HttpResponse result = execute(request);
- Assertions.assertSame(originResponse, result);
- }
-
- /*
- * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
- * Proxy-Authenticate header field (section 14.33) containing a challenge
- * applicable to the proxy for the requested resource."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
- */
- @Test
- public void testPassesOnOrigin407WithoutAProxyAuthenticateHeader() throws Exception {
- originResponse = new BasicClassicHttpResponse(407, "Proxy Authentication Required");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final HttpResponse result = execute(request);
- Assertions.assertSame(originResponse, result);
- }
-
-}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRecommendations.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRecommendations.java
index 23d80e5ea..d5c1fb520 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRecommendations.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRecommendations.java
@@ -26,7 +26,6 @@
*/
package org.apache.hc.client5.http.impl.cache;
-
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -36,14 +35,11 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
-import java.util.List;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecRuntime;
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.utils.DateUtils;
import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -55,7 +51,6 @@
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@@ -70,8 +65,8 @@
/*
* This test class captures functionality required to achieve unconditional
- * compliance with the HTTP/1.1 spec, i.e. all the SHOULD, SHOULD NOT,
- * RECOMMENDED, and NOT RECOMMENDED behaviors.
+ * compliance with the HTTP/1.1 caching protocol (SHOULD, SHOULD NOT,
+ * RECOMMENDED, and NOT RECOMMENDED behaviors).
*/
public class TestProtocolRecommendations {
@@ -133,13 +128,6 @@ public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOEx
mockExecChain);
}
- /*
- * "304 Not Modified. ... If the conditional GET used a strong cache
- * validator (see section 13.3.3), the response SHOULD NOT include
- * other entity-headers."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
final String headerName, final String headerValue, final String validatorHeader,
final String validator, final String conditionalHeader) throws Exception {
@@ -158,10 +146,8 @@ private void cacheGenerated304ForValidatorShouldNotContainEntityHeader(
execute(req1);
final ClassicHttpResponse result = execute(req2);
-
- if (HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
- assertFalse(result.containsHeader(headerName));
- }
+ assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ assertFalse(result.containsHeader(headerName));
}
private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
@@ -237,55 +223,6 @@ public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5()
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
- private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
- final String validatorHeader, final String validator, final String conditionalHeader) throws Exception {
- final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
- req1.setHeader("Range","bytes=0-127");
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader(validatorHeader, validator);
- resp1.setHeader("Content-Range", "bytes 0-127/256");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
- req2.setHeader("If-Range", validator);
- req2.setHeader("Range","bytes=0-127");
- req2.setHeader(conditionalHeader, validator);
-
- try (final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified")) {
- resp2.setHeader("Date", DateUtils.formatStandardDate(now));
- resp2.setHeader(validatorHeader, validator);
- }
-
- // cache module does not currently deal with byte ranges, but we want
- // this test to work even if it does some day
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(reqCapture.capture(), Mockito.any());
-
- final List allRequests = reqCapture.getAllValues();
- if (allRequests.isEmpty() && HttpStatus.SC_NOT_MODIFIED == result.getCode()) {
- // cache generated a 304
- assertFalse(result.containsHeader("Content-Range"));
- }
- }
-
- @Test
- public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange() throws Exception {
- cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
- "ETag", "\"etag\"", "If-None-Match");
- }
-
- @Test
- public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange() throws Exception {
- cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
- "Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo), "If-Modified-Since");
- }
-
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType() throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
@@ -310,11 +247,6 @@ public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified(
"Last-Modified", DateUtils.formatStandardDate(twoMinutesAgo));
}
- /*
- * "For this reason, a cache SHOULD NOT return a stale response if the
- * client explicitly requests a first-hand or fresh one, unless it is
- * impossible to comply for technical or policy reasons."
- */
private ClassicHttpRequest requestToPopulateStaleCacheEntry() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
@@ -397,12 +329,6 @@ public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxSt
Mockito.verify(mockExecChain, Mockito.atMost(1)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "A transparent proxy SHOULD NOT modify an end-to-end header unless
- * the definition of that header requires or specifically allows that."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
- */
private void testDoesNotModifyHeaderOnResponses(final String headerName) throws Exception {
final String headerValue = HttpTestUtils
.getCanonicalHeaderValue(originResponse, headerName);
@@ -635,14 +561,6 @@ public void testDoesNotModifyExtensionHeaderOnResponses() throws Exception {
testDoesNotModifyHeaderOnResponses("X-Extension");
}
-
- /*
- * "[HTTP/1.1 clients], If only a Last-Modified value has been provided
- * by the origin server, SHOULD use that value in non-subrange cache-
- * conditional requests (using If-Modified-Since)."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
- */
@Test
public void testUsesLastModifiedDateForCacheConditionalRequests() throws Exception {
final Instant twentySecondsAgo = now.plusSeconds(20);
@@ -673,14 +591,6 @@ public void testUsesLastModifiedDateForCacheConditionalRequests() throws Excepti
MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-Modified-Since", lmDate));
}
- /*
- * "[HTTP/1.1 clients], if both an entity tag and a Last-Modified value
- * have been provided by the origin server, SHOULD use both validators
- * in cache-conditional requests. This allows both HTTP/1.0 and
- * HTTP/1.1 caches to respond appropriately."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
- */
@Test
public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() throws Exception {
final Instant twentySecondsAgo = now.plusSeconds(20);
@@ -714,14 +624,6 @@ public void testUsesBothLastModifiedAndETagForConditionalRequestsIfAvailable() t
MatcherAssert.assertThat(captured, ContainsHeaderMatcher.contains("If-None-Match", etag));
}
- /*
- * "If an origin server wishes to force a semantically transparent cache
- * to validate every request, it MAY assign an explicit expiration time
- * in the past. This means that the response is always stale, and so the
- * cache SHOULD validate it before using it for subsequent requests."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.1
- */
@Test
public void testRevalidatesCachedResponseWithExpirationInThePast() throws Exception {
final Instant oneSecondAgo = now.minusSeconds(1);
@@ -753,19 +655,6 @@ public void testRevalidatesCachedResponseWithExpirationInThePast() throws Except
assertEquals(HttpStatus.SC_OK, result.getCode());
}
- /* "When a client tries to revalidate a cache entry, and the response
- * it receives contains a Date header that appears to be older than the
- * one for the existing entry, then the client SHOULD repeat the
- * request unconditionally, and include
- * Cache-Control: max-age=0
- * to force any intermediate caches to validate their copies directly
- * with the origin server, or
- * Cache-Control: no-cache
- * to force any intermediate caches to obtain a new copy from the
- * origin server."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.6
- */
@Test
public void testRetriesValidationThatResultsInAnOlderDated304Response() throws Exception {
final Instant elevenSecondsAgo = now.minusSeconds(11);
@@ -818,16 +707,6 @@ public void testRetriesValidationThatResultsInAnOlderDated304Response() throws E
assertFalse(captured.containsHeader("If-Unmodified-Since"));
}
- /* "If an entity tag was assigned to a cached representation, the
- * forwarded request SHOULD be conditional and include the entity
- * tags in an If-None-Match header field from all its cache entries
- * for the resource."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- * NOTE: This test no longer includes ETag headers "etag1" and "etag2"
- * as they were causing issues with stack traces when printed to console
- * or logged in the log file.
- */
@Test
public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET","/");
@@ -879,13 +758,6 @@ public void testSendsAllVariantEtagsInConditionalRequest() throws Exception {
assertTrue(foundEtag1 && foundEtag2);
}
- /* "If the entity-tag of the new response matches that of an existing
- * entry, the new response SHOULD be used to processChallenge the header fields
- * of the existing entry, and the result MUST be returned to the
- * client."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
@Test
public void testResponseToExistingVariantsUpdatesEntry() throws Exception {
@@ -948,6 +820,8 @@ public void testResponseToExistingVariantsIsCachedForFutureResponses() throws Ex
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("User-Agent", "agent2");
@@ -957,153 +831,16 @@ public void testResponseToExistingVariantsIsCachedForFutureResponses() throws Ex
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("User-Agent", "agent2");
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- /* "If any of the existing cache entries contains only partial content
- * for the associated entity, its entity-tag SHOULD NOT be included in
- * the If-None-Match header field unless the request is for a range
- * that would be fully satisfied by that entry."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
- @Test
- public void variantNegotiationsDoNotIncludeEtagsForPartialResponses() throws Exception {
- final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
- req1.setHeader("User-Agent", "agent1");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control", "max-age=3600");
- resp1.setHeader("Vary", "User-Agent");
- resp1.setHeader("ETag", "\"etag1\"");
-
- final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
- req2.setHeader("User-Agent", "agent2");
- req2.setHeader("Range", "bytes=0-49");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(50));
- resp2.setHeader("Content-Length","50");
- resp2.setHeader("Content-Range","bytes 0-49/100");
- resp2.setHeader("Vary","User-Agent");
- resp2.setHeader("ETag", "\"etag2\"");
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
-
- final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
- req3.setHeader("User-Agent", "agent3");
-
- final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control", "max-age=3600");
- resp1.setHeader("Vary", "User-Agent");
- resp1.setHeader("ETag", "\"etag3\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
execute(req2);
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
-
- execute(req3);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain, Mockito.times(3)).proceed(reqCapture.capture(), Mockito.any());
-
- final ClassicHttpRequest captured = reqCapture.getValue();
- final Iterator it = MessageSupport.iterate(captured, HttpHeaders.IF_NONE_MATCH);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- assertNotEquals("\"etag2\"", elt.toString());
- }
- }
-
- /* "If a cache receives a successful response whose Content-Location
- * field matches that of an existing cache entry for the same Request-
- * URI, whose entity-tag differs from that of the existing entry, and
- * whose Date is more recent than that of the existing entry, the
- * existing entry SHOULD NOT be returned in response to future requests
- * and SHOULD be deleted from the cache.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
- @Test
- public void cachedEntryShouldNotBeUsedIfMoreRecentMentionInContentLocation() throws Exception {
- final ClassicHttpRequest req1 = new HttpGet("http://foo.example.com/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("ETag", "\"old-etag\"");
- resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = new HttpPost("http://foo.example.com/bar");
- final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
- resp2.setHeader("ETag", "\"new-etag\"");
- resp2.setHeader("Date", DateUtils.formatStandardDate(now));
- resp2.setHeader("Content-Location", "http://foo.example.com/");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpRequest req3 = new HttpGet("http://foo.example.com");
- final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
+ final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
+ req3.setHeader("User-Agent", "agent2");
- execute(req1);
- execute(req2);
execute(req3);
- }
-
- /*
- * "This specifically means that responses from HTTP/1.0 servers for such
- * URIs [those containing a '?' in the rel_path part] SHOULD NOT be taken
- * from a cache."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
- */
- @Test
- public void responseToGetWithQueryFrom1_0OriginAndNoExpiresIsNotCached() throws Exception {
- final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp2.setVersion(HttpVersion.HTTP_1_0);
- resp2.setEntity(HttpTestUtils.makeBody(200));
- resp2.setHeader("Content-Length","200");
- resp2.setHeader("Date", DateUtils.formatStandardDate(now));
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- execute(req2);
+ Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- @Test
- public void responseToGetWithQueryFrom1_0OriginVia1_1ProxyAndNoExpiresIsNotCached() throws Exception {
- final ClassicHttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp2.setVersion(HttpVersion.HTTP_1_0);
- resp2.setEntity(HttpTestUtils.makeBody(200));
- resp2.setHeader("Content-Length","200");
- resp2.setHeader("Date", DateUtils.formatStandardDate(now));
- resp2.setHeader(HttpHeaders.VIA,"1.0 someproxy");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- execute(req2);
- }
-
- /*
- * "A cache that passes through requests for methods it does not
- * understand SHOULD invalidate any entities referred to by the
- * Request-URI."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
- */
@Test
public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -1112,6 +849,8 @@ public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Except
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("FROB", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
@@ -1124,7 +863,6 @@ public void shouldInvalidateNonvariantCacheEntryForUnknownMethod() throws Except
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
- execute(req1);
execute(req2);
final ClassicHttpResponse result = execute(req3);
@@ -1184,13 +922,6 @@ public void shouldInvalidateAllVariantsForUnknownMethod() throws Exception {
assertTrue(HttpTestUtils.semanticallyTransparent(resp5, result5));
}
- /*
- * "If a new cacheable response is received from a resource while any
- * existing responses for the same resource are cached, the cache
- * SHOULD use the new response to reply to the current request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.12
- */
@Test
public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
@@ -1201,6 +932,8 @@ public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-age=0");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -1212,24 +945,12 @@ public void cacheShouldUpdateWithNewCacheableResponse() throws Exception {
final ClassicHttpRequest req3 = HttpTestUtils.makeDefaultRequest();
- execute(req1);
execute(req2);
final ClassicHttpResponse result = execute(req3);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
- /*
- * "Many HTTP/1.0 cache implementations will treat an Expires value
- * that is less than or equal to the response Date value as being
- * equivalent to the Cache-Control response directive 'no-cache'.
- * If an HTTP/1.1 cache receives such a response, and the response
- * does not include a Cache-Control header field, it SHOULD consider
- * the response to be non-cacheable in order to retain compatibility
- * with HTTP/1.0 servers."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
- */
@Test
public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exception {
final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
@@ -1240,6 +961,8 @@ public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -1247,7 +970,6 @@ public void expiresEqualToDateWithNoCacheControlIsNotCacheable() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
final ClassicHttpResponse result = execute(req2);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
@@ -1263,6 +985,8 @@ public void expiresPriorToDateWithNoCacheControlIsNotCacheable() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -1270,21 +994,11 @@ public void expiresPriorToDateWithNoCacheControlIsNotCacheable() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
final ClassicHttpResponse result = execute(req2);
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
- /*
- * "To do this, the client may include the only-if-cached directive in
- * a request. If it receives this directive, a cache SHOULD either
- * respond using a cached entry that is consistent with the other
- * constraints of the request, or respond with a 504 (Gateway Timeout)
- * status."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
- */
@Test
public void cacheMissResultsIn504WithOnlyIfCached() throws Exception {
final ClassicHttpRequest req = HttpTestUtils.makeDefaultRequest();
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java
index bbb37cc94..73c02017d 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestProtocolRequirements.java
@@ -33,7 +33,6 @@
import java.net.SocketTimeoutException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
@@ -51,21 +50,18 @@
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HeaderElement;
-import org.apache.hc.core5.http.HeaderElements;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
-import org.apache.hc.core5.http.ProtocolVersion;
-import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
-import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.MessageSupport;
+import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -74,14 +70,9 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-/**
- * We are a conditionally-compliant HTTP/1.1 client with a cache. However, a lot
- * of the rules for proxies apply to us, as far as proper operation of the
- * requests that pass through us. Generally speaking, we want to make sure that
- * any response returned from our HttpClient.execute() methods is conditionally
- * compliant with the rules for an HTTP/1.1 server, and that any requests we
- * pass downstream to the backend HttpClient are are conditionally compliant
- * with the rules for an HTTP/1.1 client.
+/*
+ * This test class captures functionality required to achieve conditional
+ * compliance with the HTTP/1.1 caching protocol (MUST and MUST NOT behaviors).
*/
public class TestProtocolRequirements {
@@ -148,261 +139,6 @@ public void testCacheMissOnGETUsesOriginResponse() throws Exception {
Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
}
- /*
- * "Proxy and gateway applications need to be careful when forwarding
- * messages in protocol versions different from that of the application.
- * Since the protocol version indicates the protocol capability of the
- * sender, a proxy/gateway MUST NOT send a message with a version indicator
- * which is greater than its actual version. If a higher version request is
- * received, the proxy/gateway MUST either downgrade the request version, or
- * respond with an error, or switch to tunnel behavior."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
- */
- @Test
- public void testHigherMajorProtocolVersionsOnRequestSwitchToTunnelBehavior() throws Exception {
-
- // tunnel behavior: I don't muck with request or response in
- // any way
- request = new BasicClassicHttpRequest("GET", "/foo");
- request.setVersion(new ProtocolVersion("HTTP", 2, 13));
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertSame(originResponse, result);
- }
-
- @Test
- public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
-
- request = new BasicClassicHttpRequest("GET", "/foo");
- request.setVersion(new ProtocolVersion("HTTP", 1, 2));
-
- final ClassicHttpRequest downgraded = new BasicClassicHttpRequest("GET", "/foo");
- downgraded.setVersion(HttpVersion.HTTP_1_1);
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(downgraded), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
- }
-
- /*
- * "Due to interoperability problems with HTTP/1.0 proxies discovered since
- * the publication of RFC 2068[33], caching proxies MUST, gateways MAY, and
- * tunnels MUST NOT upgrade the request to the highest version they support.
- * The proxy/gateway's response to that request MUST be in the same major
- * version as the request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.1
- */
- @Test
- public void testRequestsWithLowerProtocolVersionsGetUpgradedTo1_1() throws Exception {
-
- request = new BasicClassicHttpRequest("GET", "/foo");
- request.setVersion(new ProtocolVersion("HTTP", 1, 0));
- final ClassicHttpRequest upgraded = new BasicClassicHttpRequest("GET", "/foo");
- upgraded.setVersion(HttpVersion.HTTP_1_1);
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(upgraded), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(originResponse, result));
- }
-
- /*
- * "An HTTP server SHOULD send a response version equal to the highest
- * version for which the server is at least conditionally compliant, and
- * whose major version is less than or equal to the one received in the
- * request."
- *
- * http://www.ietf.org/rfc/rfc2145.txt
- */
- @Test
- public void testLowerOriginResponsesUpgradedToOurVersion1_1() throws Exception {
- originResponse = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- originResponse.setVersion(new ProtocolVersion("HTTP", 1, 2));
- originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- originResponse.setHeader("Server", "MockOrigin/1.0");
- originResponse.setEntity(body);
-
- // not testing this internal behavior in this test, just want
- // to check the protocol version that comes out the other end
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertEquals(HttpVersion.HTTP_1_1, result.getVersion());
- }
-
- @Test
- public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
- request = new BasicClassicHttpRequest("GET", "/foo");
- request.setVersion(new ProtocolVersion("HTTP", 1, 0));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertEquals(HttpVersion.HTTP_1_1, result.getVersion());
- }
-
- /*
- * "A proxy MUST forward an unknown header, unless it is protected by a
- * Connection header." http://www.ietf.org/rfc/rfc2145.txt
- */
- @Test
- public void testForwardsUnknownHeadersOnRequestsFromHigherProtocolVersions() throws Exception {
- request = new BasicClassicHttpRequest("GET", "/foo");
- request.setVersion(new ProtocolVersion("HTTP", 1, 2));
- request.removeHeaders("Connection");
- request.addHeader("X-Unknown-Header", "some-value");
-
- final ClassicHttpRequest downgraded = new BasicClassicHttpRequest("GET", "/foo");
- downgraded.setVersion(HttpVersion.HTTP_1_1);
- downgraded.removeHeaders("Connection");
- downgraded.addHeader("X-Unknown-Header", "some-value");
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(downgraded), Mockito.any())).thenReturn(originResponse);
-
- execute(request);
- }
-
- /*
- * "A server MUST NOT send transfer-codings to an HTTP/1.0 client."
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6
- */
- @Test
- public void testTransferCodingsAreNotSentToAnHTTP_1_0Client() throws Exception {
-
- originResponse.setHeader("Transfer-Encoding", "identity");
-
- final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/foo");
- originalRequest.setVersion(new ProtocolVersion("HTTP", 1, 0));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(originalRequest);
-
- Assertions.assertNull(result.getFirstHeader("TE"));
- Assertions.assertNull(result.getFirstHeader("Transfer-Encoding"));
- }
-
- /*
- * "Multiple message-header fields with the same field-name MAY be present
- * in a message if and only if the entire field-value for that header field
- * is defined as a comma-separated list [i.e., #(values)]. It MUST be
- * possible to combine the multiple header fields into one
- * "field-name: field-value" pair, without changing the semantics of the
- * message, by appending each subsequent field-value to the first, each
- * separated by a comma. The order in which header fields with the same
- * field-name are received is therefore significant to the interpretation of
- * the combined field value, and thus a proxy MUST NOT change the order of
- * these field values when a message is forwarded."
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
- */
- private void testOrderOfMultipleHeadersIsPreservedOnRequests(final String h, final ClassicHttpRequest request) throws Exception {
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(request);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
-
- final ClassicHttpRequest forwarded = reqCapture.getValue();
- final String expected = HttpTestUtils.getCanonicalHeaderValue(request, h);
- final String actual = HttpTestUtils.getCanonicalHeaderValue(forwarded, h);
- if (!actual.contains(expected)) {
- Assertions.assertEquals(expected, actual);
- }
- }
-
- @Test
- public void testOrderOfMultipleAcceptHeaderValuesIsPreservedOnRequests() throws Exception {
- request.addHeader("Accept", "audio/*; q=0.2, audio/basic");
- request.addHeader("Accept", "text/*, text/html, text/html;level=1, */*");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Accept", request);
- }
-
- @Test
- public void testOrderOfMultipleAcceptCharsetHeadersIsPreservedOnRequests() throws Exception {
- request.addHeader("Accept-Charset", "iso-8859-5");
- request.addHeader("Accept-Charset", "unicode-1-1;q=0.8");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Charset", request);
- }
-
- @Test
- public void testOrderOfMultipleAcceptEncodingHeadersIsPreservedOnRequests() throws Exception {
- request.addHeader("Accept-Encoding", "identity");
- request.addHeader("Accept-Encoding", "compress, gzip");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
- }
-
- @Test
- public void testOrderOfMultipleAcceptLanguageHeadersIsPreservedOnRequests() throws Exception {
- request.addHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7");
- request.addHeader("Accept-Language", "i-cherokee");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Accept-Encoding", request);
- }
-
- @Test
- public void testOrderOfMultipleAllowHeadersIsPreservedOnRequests() throws Exception {
- final BasicClassicHttpRequest put = new BasicClassicHttpRequest("PUT", "/");
- put.setEntity(body);
- put.addHeader("Allow", "GET, HEAD");
- put.addHeader("Allow", "DELETE");
- put.addHeader("Content-Length", "128");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Allow", put);
- }
-
- @Test
- public void testOrderOfMultipleCacheControlHeadersIsPreservedOnRequests() throws Exception {
- request.addHeader("Cache-Control", "max-age=5");
- request.addHeader("Cache-Control", "min-fresh=10");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Cache-Control", request);
- }
-
- @Test
- public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnRequests() throws Exception {
- final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
- post.setEntity(body);
- post.addHeader("Content-Encoding", "gzip");
- post.addHeader("Content-Encoding", "compress");
- post.addHeader("Content-Length", "128");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Encoding", post);
- }
-
- @Test
- public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnRequests() throws Exception {
- final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
- post.setEntity(body);
- post.addHeader("Content-Language", "mi");
- post.addHeader("Content-Language", "en");
- post.addHeader("Content-Length", "128");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Content-Language", post);
- }
-
- @Test
- public void testOrderOfMultipleExpectHeadersIsPreservedOnRequests() throws Exception {
- final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
- post.setEntity(body);
- post.addHeader("Expect", "100-continue");
- post.addHeader("Expect", "x-expect=true");
- post.addHeader("Content-Length", "128");
- testOrderOfMultipleHeadersIsPreservedOnRequests("Expect", post);
- }
-
- @Test
- public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
- request.addHeader(HttpHeaders.VIA, "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
- request.addHeader(HttpHeaders.VIA, "1.0 ricky, 1.1 mertz, 1.0 lucy");
- testOrderOfMultipleHeadersIsPreservedOnRequests(HttpHeaders.VIA, request);
- }
-
private void testOrderOfMultipleHeadersIsPreservedOnResponses(final String h) throws Exception {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
@@ -457,14 +193,6 @@ public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() th
testOrderOfMultipleHeadersIsPreservedOnResponses("WWW-Authenticate");
}
- /*
- * "However, applications MUST understand the class of any status code, as
- * indicated by the first digit, and treat any unrecognized response as
- * being equivalent to the x00 status code of that class, with the exception
- * that an unrecognized response MUST NOT be cached."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
- */
private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exception {
originResponse = new BasicClassicHttpResponse(code, "Moo");
@@ -483,7 +211,7 @@ private void testUnknownResponseStatusCodeIsNotCached(final int code) throws Exc
@Test
public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
- for (int i = 102; i <= 199; i++) {
+ for (int i = 100; i <= 199; i++) {
testUnknownResponseStatusCodeIsNotCached(i);
}
for (int i = 207; i <= 299; i++) {
@@ -500,12 +228,6 @@ public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
}
}
- /*
- * "Unrecognized header fields SHOULD be ignored by the recipient and MUST
- * be forwarded by transparent proxies."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1
- */
@Test
public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
request.addHeader("X-Unknown-Header", "blahblah");
@@ -516,9 +238,7 @@ public void testUnknownHeadersOnRequestsAreForwarded() throws Exception {
final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
final ClassicHttpRequest forwarded = reqCapture.getValue();
- final Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
- Assertions.assertEquals(1, hdrs.length);
- Assertions.assertEquals("blahblah", hdrs[0].getValue());
+ MatcherAssert.assertThat(forwarded, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
}
@Test
@@ -527,102 +247,9 @@ public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
final ClassicHttpResponse result = execute(request);
-
- final Header[] hdrs = result.getHeaders("X-Unknown-Header");
- Assertions.assertEquals(1, hdrs.length);
- Assertions.assertEquals("blahblah", hdrs[0].getValue());
- }
-
- /*
- * "If a client will wait for a 100 (Continue) response before sending the
- * request body, it MUST send an Expect request-header field (section 14.20)
- * with the '100-continue' expectation."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
- */
- @Test
- public void testRequestsExpecting100ContinueBehaviorShouldSetExpectHeader() throws Exception {
- final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
- post.setHeader(HttpHeaders.EXPECT, HeaderElements.CONTINUE);
- post.setHeader("Content-Length", "128");
- post.setEntity(new StringEntity("whatever"));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(post);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
- final ClassicHttpRequest forwarded = reqCapture.getValue();
- boolean foundExpect = false;
- final Iterator it = MessageSupport.iterate(forwarded, HttpHeaders.EXPECT);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- if ("100-continue".equalsIgnoreCase(elt.getName())) {
- foundExpect = true;
- break;
- }
- }
- Assertions.assertTrue(foundExpect);
+ MatcherAssert.assertThat(result, ContainsHeaderMatcher.contains("X-Unknown-Header", "blahblah"));
}
- /*
- * "If a client will wait for a 100 (Continue) response before sending the
- * request body, it MUST send an Expect request-header field (section 14.20)
- * with the '100-continue' expectation."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
- */
- @Test
- public void testRequestsNotExpecting100ContinueBehaviorShouldNotSetExpectContinueHeader() throws Exception {
- final BasicClassicHttpRequest post = new BasicClassicHttpRequest("POST", "/");
- post.setHeader("Content-Length", "128");
- post.setEntity(new StringEntity("whatever"));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(post);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
- final ClassicHttpRequest forwarded = reqCapture.getValue();
- boolean foundExpect = false;
- final Iterator it = MessageSupport.iterate(forwarded, HttpHeaders.EXPECT);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- if ("100-continue".equalsIgnoreCase(elt.getName())) {
- foundExpect = true;
- break;
- }
- }
- Assertions.assertFalse(foundExpect);
- }
-
- /*
- * "If a proxy receives a request that includes an Expect request- header
- * field with the '100-continue' expectation, and the proxy either knows
- * that the next-hop server complies with HTTP/1.1 or higher, or does not
- * know the HTTP version of the next-hop server, it MUST forward the
- * request, including the Expect header field.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
- */
- @Test
- public void testExpectHeadersAreForwardedOnRequests() throws Exception {
- // This would mostly apply to us if we were part of an
- // application that was a proxy, and would be the
- // responsibility of the greater application. Our
- // responsibility is to make sure that if we get an
- // entity-enclosing request that we properly set (or unset)
- // the Expect header per the request.expectContinue() flag,
- // which is tested by the previous few tests.
- }
-
- /*
- * "9.2 OPTIONS. ...Responses to this method are not cacheable.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
- */
@Test
public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
request = new BasicClassicHttpRequest("OPTIONS", "/");
@@ -635,152 +262,6 @@ public void testResponsesToOPTIONSAreNotCacheable() throws Exception {
Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "A 200 response SHOULD .... If no response body is included, the response
- * MUST include a Content-Length field with a field-value of '0'."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
- */
- @Test
- public void test200ResponseToOPTIONSWithNoBodyShouldIncludeContentLengthZero() throws Exception {
-
- request = new BasicClassicHttpRequest("OPTIONS", "/");
- originResponse.setEntity(null);
- originResponse.setHeader("Content-Length", "0");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- final Header contentLength = result.getFirstHeader("Content-Length");
- Assertions.assertNotNull(contentLength);
- Assertions.assertEquals("0", contentLength.getValue());
- }
-
- /*
- * "When a proxy receives an OPTIONS request on an absoluteURI for which
- * request forwarding is permitted, the proxy MUST check for a Max-Forwards
- * field. If the Max-Forwards field-value is zero ("0"), the proxy MUST NOT
- * forward the message; instead, the proxy SHOULD respond with its own
- * communication options."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
- */
- @Test
- public void testDoesNotForwardOPTIONSWhenMaxForwardsIsZeroOnAbsoluteURIRequest() throws Exception {
- request = new BasicClassicHttpRequest("OPTIONS", "*");
- request.setHeader("Max-Forwards", "0");
-
- execute(request);
- }
-
- /*
- * "If no Max-Forwards field is present in the request, then the forwarded
- * request MUST NOT include a Max-Forwards field."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
- */
- @Test
- public void testDoesNotAddAMaxForwardsHeaderToForwardedOPTIONSRequests() throws Exception {
- request = new BasicClassicHttpRequest("OPTIONS", "/");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(request);
-
- final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
- Mockito.verify(mockExecChain).proceed(reqCapture.capture(), Mockito.any());
-
- final ClassicHttpRequest forwarded = reqCapture.getValue();
- Assertions.assertNull(forwarded.getFirstHeader("Max-Forwards"));
- }
-
- /*
- * "The HEAD method is identical to GET except that the server MUST NOT
- * return a message-body in the response."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
- */
- @Test
- public void testResponseToAHEADRequestMustNotHaveABody() throws Exception {
- request = new BasicClassicHttpRequest("HEAD", "/");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
- }
-
- /*
- * "If the new field values indicate that the cached entity differs from the
- * current entity (as would be indicated by a change in Content-Length,
- * Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache
- * entry as stale."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
- */
- private void testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale(final String eHeader,
- final String oldVal, final String newVal) throws Exception {
-
- // put something cacheable in the cache
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.addHeader("Cache-Control", "max-age=3600");
- resp1.setHeader(eHeader, oldVal);
-
- // get a head that penetrates the cache
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("HEAD", "/");
- req2.addHeader("Cache-Control", "no-cache");
- final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
- resp2.setEntity(null);
- resp2.setHeader(eHeader, newVal);
-
- // next request doesn't tolerate stale entry
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.addHeader("Cache-Control", "max-stale=0");
- final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
- resp3.setHeader(eHeader, newVal);
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req1), Mockito.any())).thenReturn(originResponse);
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(originResponse);
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req3), Mockito.any())).thenReturn(resp3);
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- @Test
- public void testHEADResponseWithUpdatedContentLengthFieldMakeACacheEntryStale() throws Exception {
- testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-Length", "128", "127");
- }
-
- @Test
- public void testHEADResponseWithUpdatedContentMD5FieldMakeACacheEntryStale() throws Exception {
- testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Content-MD5",
- "Q2hlY2sgSW50ZWdyaXR5IQ==", "Q2hlY2sgSW50ZWdyaXR5IR==");
-
- }
-
- @Test
- public void testHEADResponseWithUpdatedETagFieldMakeACacheEntryStale() throws Exception {
- testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("ETag", "\"etag1\"",
- "\"etag2\"");
- }
-
- @Test
- public void testHEADResponseWithUpdatedLastModifiedFieldMakeACacheEntryStale() throws Exception {
- final Instant now = Instant.now();
- final Instant tenSecondsAgo = now.minusSeconds(10);
- final Instant sixSecondsAgo = now.minusSeconds(6);
- testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils.formatStandardDate(tenSecondsAgo), DateUtils.formatStandardDate(sixSecondsAgo));
- }
-
- /*
- * "9.5 POST. Responses to this method are not cacheable, unless the
- * response includes appropriate Cache-Control or Expires header fields."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5
- */
@Test
public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws Exception {
@@ -798,11 +279,6 @@ public void testResponsesToPOSTWithoutCacheControlOrExpiresAreNotCached() throws
Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "9.5 PUT. ...Responses to this method are not cacheable."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
- */
@Test
public void testResponsesToPUTsAreNotCached() throws Exception {
@@ -819,11 +295,6 @@ public void testResponsesToPUTsAreNotCached() throws Exception {
Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "9.6 DELETE. ... Responses to this method are not cacheable."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7
- */
@Test
public void testResponsesToDELETEsAreNotCached() throws Exception {
@@ -837,11 +308,6 @@ public void testResponsesToDELETEsAreNotCached() throws Exception {
Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "9.8 TRACE ... Responses to this method MUST NOT be cached."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
- */
@Test
public void testResponsesToTRACEsAreNotCached() throws Exception {
@@ -855,534 +321,84 @@ public void testResponsesToTRACEsAreNotCached() throws Exception {
Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "The [206] response MUST include the following header fields:
- *
- * - Either a Content-Range header field (section 14.16) indicating the
- * range included with this response, or a multipart/byteranges Content-Type
- * including Content-Range fields for each part. If a Content-Length header
- * field is present in the response, its value MUST match the actual number
- * of OCTETs transmitted in the message-body.
- *
- * - Date
- *
- * - ETag and/or Content-Location, if the header would have been sent in a
- * 200 response to the same request
- *
- * - Expires, Cache-Control, and/or Vary, if the field-value might differ
- * from that sent in any previous response for the same variant"
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
- */
- @Test
- public void test206ResponseGeneratedFromCacheMustHaveContentRangeOrMultipartByteRangesContentType() throws Exception {
+ @Test
+ public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("ETag", "\"etag\"");
- resp1.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range", "bytes=0-50");
+ req2.setHeader("If-None-Match", "\"etag\"");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
- if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
- if (result.getFirstHeader("Content-Range") == null) {
- final HeaderElement elt = MessageSupport.parse(result.getFirstHeader("Content-Type"))[0];
- Assertions.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
- Assertions.assertNotNull(elt.getParameterByName("boundary"));
- Assertions.assertNotNull(elt.getParameterByName("boundary").getValue());
- Assertions.assertNotEquals("", elt.getParameterByName("boundary").getValue().trim());
- }
- }
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ Assertions.assertNotNull(result.getFirstHeader("Date"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
- public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent() throws Exception {
-
+ public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("ETag", "\"etag\"");
- resp1.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range", "bytes=0-50");
+ req2.setHeader("If-None-Match", "\"etag\"");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
- if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
- final Header h = result.getFirstHeader("Content-Length");
- if (h != null) {
- final int contentLength = Integer.parseInt(h.getValue());
- int bytesRead = 0;
- final InputStream i = result.getEntity().getContent();
- while ((i.read()) != -1) {
- bytesRead++;
- }
- i.close();
- Assertions.assertEquals(contentLength, bytesRead);
- }
- }
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ Assertions.assertNotNull(result.getFirstHeader("ETag"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
- public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
+ public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("ETag", "\"etag\"");
- resp1.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setHeader("Content-Location", "http://foo.example.com/other");
+ originResponse.setHeader("ETag", "\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range", "bytes=0-50");
+ req2.setHeader("If-None-Match", "\"etag\"");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
execute(req1);
final ClassicHttpResponse result = execute(req2);
- if (HttpStatus.SC_PARTIAL_CONTENT == result.getCode()) {
- Assertions.assertNotNull(result.getFirstHeader("Date"));
- }
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ Assertions.assertNotNull(result.getFirstHeader("Content-Location"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
@Test
- public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
+ public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer() throws Exception {
+
+ final Instant now = Instant.now();
+ final Instant inTwoHours = now.plus(2, ChronoUnit.HOURS);
+
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
+ req1.setHeader("Accept-Encoding", "gzip");
- originResponse.addHeader("Cache-Control", "max-age=3600");
- originResponse.addHeader("ETag", "\"etag1\"");
+ final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
+ resp1.setHeader("ETag", "\"v1\"");
+ resp1.setHeader("Cache-Control", "max-age=7200");
+ resp1.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
+ resp1.setHeader("Vary", "Accept-Encoding");
+ resp1.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.addHeader("Range", "bytes=0-50");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
- Assertions.assertNotNull(result.getFirstHeader("ETag"));
- }
- Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
- }
-
- @Test
- public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
-
- originResponse.addHeader("Cache-Control", "max-age=3600");
- originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.addHeader("Range", "bytes=0-50");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
- Assertions.assertNotNull(result.getFirstHeader("Content-Location"));
- }
- Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
- }
-
- @Test
- public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.addHeader("Accept-Encoding", "gzip");
-
- final Instant now = Instant.now();
- final Instant inOneHour = Instant.now().plus(1, ChronoUnit.HOURS);
- originResponse.addHeader("Cache-Control", "max-age=3600");
- originResponse.addHeader("Expires", DateUtils.formatStandardDate(inOneHour));
- originResponse.addHeader("Vary", "Accept-Encoding");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.addHeader("Cache-Control", "no-cache");
- req2.addHeader("Accept-Encoding", "gzip");
- final Instant nextSecond = Instant.now().plusSeconds(1);
- final Instant inTwoHoursPlusASec = now.plus(2, ChronoUnit.HOURS).plus(1, ChronoUnit.SECONDS);
-
- final ClassicHttpResponse originResponse2 = HttpTestUtils.make200Response();
- originResponse2.setHeader("Date", DateUtils.formatStandardDate(nextSecond));
- originResponse2.setHeader("Cache-Control", "max-age=7200");
- originResponse2.setHeader("Expires", DateUtils.formatStandardDate(inTwoHoursPlusASec));
- originResponse2.setHeader("Vary", "Accept-Encoding");
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.addHeader("Range", "bytes=0-50");
- req3.addHeader("Accept-Encoding", "gzip");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse2);
-
- execute(req2);
- final ClassicHttpResponse result = execute(req3);
-
-
- if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
- Assertions.assertNotNull(result.getFirstHeader("Expires"));
- Assertions.assertNotNull(result.getFirstHeader("Cache-Control"));
- Assertions.assertNotNull(result.getFirstHeader("Vary"));
- }
- Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
- }
-
- /*
- * "Otherwise, the [206] response MUST include all of the entity-headers
- * that would have been returned with a 200 (OK) response to the same
- * [If-Range] request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
- */
- @Test
- public void test206ResponseToIfRangeWithStrongValidatorReturnsAllEntityHeaders() throws Exception {
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
-
- final Instant now = Instant.now();
- final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
- originResponse.addHeader("Allow", "GET,HEAD");
- originResponse.addHeader("Cache-Control", "max-age=3600");
- originResponse.addHeader("Content-Language", "en");
- originResponse.addHeader("Content-Encoding", "x-coding");
- originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
- originResponse.addHeader("Content-Length", "128");
- originResponse.addHeader("Content-Type", "application/octet-stream");
- originResponse.addHeader("Last-Modified", DateUtils.formatStandardDate(oneHourAgo));
- originResponse.addHeader("ETag", "\"strong-tag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.addHeader("If-Range", "\"strong-tag\"");
- req2.addHeader("Range", "bytes=0-50");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_PARTIAL_CONTENT) {
- Assertions.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
- Assertions.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
- Assertions.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
- Assertions.assertEquals("x-coding", result.getFirstHeader("Content-Encoding").getValue());
- Assertions.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
- .getValue());
- Assertions.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
- .getFirstHeader("Last-Modified").getValue());
- }
- Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
- }
-
- /*
- * "A cache MUST NOT combine a 206 response with other previously cached
- * content if the ETag or Last-Modified headers do not match exactly, see
- * 13.5.4."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
- */
- @Test
- public void test206ResponseIsNotCombinedWithPreviousContentIfETagDoesNotMatch() throws Exception {
-
- final Instant now = Instant.now();
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control", "max-age=3600");
- resp1.setHeader("ETag", "\"etag1\"");
- final byte[] bytes1 = new byte[128];
- Arrays.fill(bytes1, (byte) 1);
- resp1.setEntity(new ByteArrayEntity(bytes1, null));
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control", "no-cache");
- req2.setHeader("Range", "bytes=0-50");
-
- final Instant inOneSecond = now.plusSeconds(1);
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,
- "Partial Content");
- resp2.setHeader("Date", DateUtils.formatStandardDate(inOneSecond));
- resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
- resp2.setHeader("ETag", "\"etag2\"");
- resp2.setHeader("Content-Range", "bytes 0-50/128");
- final byte[] bytes2 = new byte[51];
- Arrays.fill(bytes2, (byte) 2);
- resp2.setEntity(new ByteArrayEntity(bytes2, null));
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- execute(req2);
-
- final ClassicHttpResponse result = execute(req3);
-
- final InputStream i = result.getEntity().getContent();
- int b;
- boolean found1 = false;
- boolean found2 = false;
- while ((b = i.read()) != -1) {
- if (b == 1) {
- found1 = true;
- }
- if (b == 2) {
- found2 = true;
- }
- }
- i.close();
- Assertions.assertFalse(found1 && found2); // mixture of content
- Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
- }
-
- @Test
- public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch() throws Exception {
-
- final Instant now = Instant.now();
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- final Instant oneHourAgo = now.minus(1, ChronoUnit.HOURS);
- resp1.setHeader("Cache-Control", "max-age=3600");
- resp1.setHeader("Last-Modified", DateUtils.formatStandardDate(oneHourAgo));
- final byte[] bytes1 = new byte[128];
- Arrays.fill(bytes1, (byte) 1);
- resp1.setEntity(new ByteArrayEntity(bytes1, null));
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control", "no-cache");
- req2.setHeader("Range", "bytes=0-50");
-
- final Instant inOneSecond = now.plusSeconds(1);
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,
- "Partial Content");
- resp2.setHeader("Date", DateUtils.formatStandardDate(inOneSecond));
- resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
- resp2.setHeader("Last-Modified", DateUtils.formatStandardDate(now));
- resp2.setHeader("Content-Range", "bytes 0-50/128");
- final byte[] bytes2 = new byte[51];
- Arrays.fill(bytes2, (byte) 2);
- resp2.setEntity(new ByteArrayEntity(bytes2, null));
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- execute(req2);
-
- final ClassicHttpResponse result = execute(req3);
-
- final InputStream i = result.getEntity().getContent();
- int b;
- boolean found1 = false;
- boolean found2 = false;
- while ((b = i.read()) != -1) {
- if (b == 1) {
- found1 = true;
- }
- if (b == 2) {
- found2 = true;
- }
- }
- i.close();
- Assertions.assertFalse(found1 && found2); // mixture of content
- Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
- }
-
- /*
- * "A cache that does not support the Range and Content-Range headers MUST
- * NOT cache 206 (Partial) responses."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
- */
- @Test
- public void test206ResponsesAreNotCachedIfTheCacheDoesNotSupportRangeAndContentRangeHeaders() throws Exception {
-
- if (!impl.supportsRangeAndContentRangeHeaders()) {
- request = new BasicClassicHttpRequest("GET", "/");
- request.addHeader("Range", "bytes=0-50");
-
- originResponse = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT,"Partial Content");
- originResponse.setHeader("Content-Range", "bytes 0-50/128");
- originResponse.setHeader("Cache-Control", "max-age=3600");
- final byte[] bytes = new byte[51];
- new Random().nextBytes(bytes);
- originResponse.setEntity(new ByteArrayEntity(bytes, null));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(request);
- Mockito.verifyNoInteractions(mockCache);
- }
- }
-
- /*
- * "10.3.4 303 See Other ... The 303 response MUST NOT be cached, but the
- * response to the second (redirected) request might be cacheable."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
- */
- @Test
- public void test303ResponsesAreNotCached() throws Exception {
-
- request = new BasicClassicHttpRequest("GET", "/");
-
- originResponse = new BasicClassicHttpResponse(HttpStatus.SC_SEE_OTHER, "See Other");
- originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- originResponse.setHeader("Server", "MockServer/1.0");
- originResponse.setHeader("Cache-Control", "max-age=3600");
- originResponse.setHeader("Content-Type", "application/x-cachingclient-test");
- originResponse.setHeader("Location", "http://foo.example.com/other");
- originResponse.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(request);
-
- Mockito.verifyNoInteractions(mockCache);
- }
-
- /*
- * "The [304] response MUST include the following header fields: - Date,
- * unless its omission is required by section 14.18.1 [clockless origin
- * servers]."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
- @Test
- public void test304ResponseWithDateHeaderForwardedFromOriginIncludesDateHeader() throws Exception {
-
- request.setHeader("If-None-Match", "\"etag\"");
-
- originResponse = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED,"Not Modified");
- originResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- originResponse.setHeader("Server", "MockServer/1.0");
- originResponse.setHeader("ETag", "\"etag\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertNotNull(result.getFirstHeader("Date"));
- }
-
- @Test
- public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- originResponse.setHeader("Cache-Control", "max-age=3600");
- originResponse.setHeader("ETag", "\"etag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("If-None-Match", "\"etag\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
- Assertions.assertNotNull(result.getFirstHeader("Date"));
- }
- Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
- }
-
- /*
- * "The [304] response MUST include the following header fields: - ETag
- * and/or Content-Location, if the header would have been sent in a 200
- * response to the same request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
- @Test
- public void test304ResponseGeneratedFromCacheIncludesEtagIfOriginResponseDid() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- originResponse.setHeader("Cache-Control", "max-age=3600");
- originResponse.setHeader("ETag", "\"etag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("If-None-Match", "\"etag\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
- Assertions.assertNotNull(result.getFirstHeader("ETag"));
- }
- Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
- }
-
- @Test
- public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- originResponse.setHeader("Cache-Control", "max-age=3600");
- originResponse.setHeader("Content-Location", "http://foo.example.com/other");
- originResponse.setHeader("ETag", "\"etag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("If-None-Match", "\"etag\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
- Assertions.assertNotNull(result.getFirstHeader("Content-Location"));
- }
- Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
- }
-
- /*
- * "The [304] response MUST include the following header fields: ... -
- * Expires, Cache-Control, and/or Vary, if the field-value might differ from
- * that sent in any previous response for the same variant
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
- @Test
- public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVaryIfResponseMightDiffer() throws Exception {
-
- final Instant now = Instant.now();
- final Instant inTwoHours = now.plus(2, ChronoUnit.HOURS);
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Accept-Encoding", "gzip");
-
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("ETag", "\"v1\"");
- resp1.setHeader("Cache-Control", "max-age=7200");
- resp1.setHeader("Expires", DateUtils.formatStandardDate(inTwoHours));
- resp1.setHeader("Vary", "Accept-Encoding");
- resp1.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Accept-Encoding", "gzip");
- req1.setHeader("Cache-Control", "no-cache");
+ req2.setHeader("Accept-Encoding", "gzip");
+ req2.setHeader("Cache-Control", "no-cache");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"v2\"");
@@ -1400,25 +416,17 @@ public void test304ResponseGeneratedFromCacheIncludesExpiresCacheControlAndOrVar
execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
execute(req2);
+
final ClassicHttpResponse result = execute(req3);
- if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
- Assertions.assertNotNull(result.getFirstHeader("Expires"));
- Assertions.assertNotNull(result.getFirstHeader("Cache-Control"));
- Assertions.assertNotNull(result.getFirstHeader("Vary"));
- }
- Mockito.verify(mockExecChain, Mockito.times(3)).proceed(Mockito.any(), Mockito.any());
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ Assertions.assertNotNull(result.getFirstHeader("Expires"));
+ Assertions.assertNotNull(result.getFirstHeader("Cache-Control"));
+ Assertions.assertNotNull(result.getFirstHeader("Vary"));
+ Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "Otherwise (i.e., the conditional GET used a weak validator), the
- * response MUST NOT include other entity-headers; this prevents
- * inconsistencies between cached entity-bodies and updated headers."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
@Test
public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHeaders() throws Exception {
@@ -1446,24 +454,16 @@ public void test304GeneratedFromCacheOnWeakValidatorDoesNotIncludeOtherEntityHea
execute(req1);
final ClassicHttpResponse result = execute(req2);
- if (result.getCode() == HttpStatus.SC_NOT_MODIFIED) {
- Assertions.assertNull(result.getFirstHeader("Allow"));
- Assertions.assertNull(result.getFirstHeader("Content-Encoding"));
- Assertions.assertNull(result.getFirstHeader("Content-Length"));
- Assertions.assertNull(result.getFirstHeader("Content-MD5"));
- Assertions.assertNull(result.getFirstHeader("Content-Type"));
- Assertions.assertNull(result.getFirstHeader("Last-Modified"));
- }
+ Assertions.assertEquals(HttpStatus.SC_NOT_MODIFIED, result.getCode());
+ Assertions.assertNull(result.getFirstHeader("Allow"));
+ Assertions.assertNull(result.getFirstHeader("Content-Encoding"));
+ Assertions.assertNull(result.getFirstHeader("Content-Length"));
+ Assertions.assertNull(result.getFirstHeader("Content-MD5"));
+ Assertions.assertNull(result.getFirstHeader("Content-Type"));
+ Assertions.assertNull(result.getFirstHeader("Last-Modified"));
Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "If a 304 response indicates an entity not currently cached, then the
- * cache MUST disregard the response and repeat the request without the
- * conditional."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
@Test
public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET() throws Exception {
@@ -1498,13 +498,6 @@ public void testNotModifiedOfNonCachedEntityShouldRevalidateWithUnconditionalGET
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "If a cache uses a received 304 response to processChallenge a cache entry, the
- * cache MUST processChallenge the entry to reflect any new field values given in the
- * response.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- */
@Test
public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exception {
@@ -1547,90 +540,12 @@ public void testCacheEntryIsUpdatedWithNewFieldValuesIn304Response() throws Exce
Assertions.assertEquals("junk", result.getFirstHeader("X-Extra").getValue());
}
- /*
- * "10.4.2 401 Unauthorized ... The response MUST include a WWW-Authenticate
- * header field (section 14.47) containing a challenge applicable to the
- * requested resource."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
- */
@Test
- public void testMustIncludeWWWAuthenticateHeaderOnAnOrigin401Response() throws Exception {
- originResponse = new BasicClassicHttpResponse(401, "Unauthorized");
- originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
+ public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses() throws Exception {
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
- Assertions.assertEquals(401, result.getCode());
- Assertions.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
- }
-
- /*
- * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
- * header containing a list of valid methods for the requested resource.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
- */
- @Test
- public void testMustIncludeAllowHeaderFromAnOrigin405Response() throws Exception {
- originResponse = new BasicClassicHttpResponse(405, "Method Not Allowed");
- originResponse.setHeader("Allow", "GET, HEAD");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
- Assertions.assertEquals(405, result.getCode());
- Assertions.assertNotNull(result.getFirstHeader("Allow"));
- }
-
- /*
- * "10.4.8 407 Proxy Authentication Required ... The proxy MUST return a
- * Proxy-Authenticate header field (section 14.33) containing a challenge
- * applicable to the proxy for the requested resource."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
- */
- @Test
- public void testMustIncludeProxyAuthenticateHeaderFromAnOrigin407Response() throws Exception {
- originResponse = new BasicClassicHttpResponse(407, "Proxy Authentication Required");
- originResponse.setHeader("Proxy-Authenticate", "x-scheme x-param");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
- Assertions.assertEquals(407, result.getCode());
- Assertions.assertNotNull(result.getFirstHeader("Proxy-Authenticate"));
- }
-
- /*
- * "10.4.17 416 Requested Range Not Satisfiable ... This response MUST NOT
- * use the multipart/byteranges content-type."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
- */
- @Test
- public void testMustNotAddMultipartByteRangeContentTypeTo416Response() throws Exception {
- originResponse = new BasicClassicHttpResponse(416, "Requested Range Not Satisfiable");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
-
- Assertions.assertEquals(416, result.getCode());
- final Iterator it = MessageSupport.iterate(result, HttpHeaders.CONTENT_TYPE);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- Assertions.assertFalse("multipart/byteranges".equalsIgnoreCase(elt.getName()));
- }
- }
-
- @Test
- public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses() throws Exception {
-
- originResponse.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
- originResponse.setHeader("Content-Length", "128");
- originResponse.setHeader("Cache-Control", "max-age=3600");
+ originResponse.setEntity(HttpTestUtils.makeBody(ENTITY_LENGTH));
+ originResponse.setHeader("Content-Length", "128");
+ originResponse.setHeader("Cache-Control", "max-age=3600");
final ClassicHttpRequest rangeReq = new BasicClassicHttpRequest("GET", "/");
rangeReq.setHeader("Range", "bytes=1000-1200");
@@ -1656,34 +571,6 @@ public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Respon
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "A correct cache MUST respond to a request with the most up-to-date
- * response held by the cache that is appropriate to the request (see
- * sections 13.2.5, 13.2.6, and 13.12) which meets one of the following
- * conditions:
- *
- * 1. It has been checked for equivalence with what the origin server would
- * have returned by revalidating the response with the origin server
- * (section 13.3);
- *
- * 2. It is "fresh enough" (see section 13.2). In the default case, this
- * means it meets the least restrictive freshness requirement of the client,
- * origin server, and cache (see section 14.9); if the origin server so
- * specifies, it is the freshness requirement of the origin server alone.
- *
- * If a stored response is not "fresh enough" by the most restrictive
- * freshness requirement of both the client and the origin server, in
- * carefully considered circumstances the cache MAY still return the
- * response with the appropriate Warning header (see section 13.1.5 and
- * 14.46), unless such a response is prohibited (e.g., by a "no-store"
- * cache-directive, or by a "no-cache" cache-request-directive; see section
- * 14.9).
- *
- * 3. It is an appropriate 304 (Not Modified), 305 (Proxy Redirect), or
- * error (4xx or 5xx) response message."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
- */
@Test
public void testMustReturnACacheEntryIfItCanRevalidateIt() throws Exception {
@@ -1768,13 +655,6 @@ public void testMustReturnAFreshEnoughCacheEntryIfItHasIt() throws Exception {
Assertions.assertEquals(200, result.getCode());
}
- /*
- * "When a response is generated from a cache entry, the cache MUST include
- * a single Age header field in the response with a value equal to the cache
- * entry's current_age."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.3
- */
@Test
public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
@@ -1812,13 +692,6 @@ public void testAgeHeaderPopulatedFromCacheEntryCurrentAge() throws Exception {
assertThat(result, ContainsHeaderMatcher.contains("Age", "10"));
}
- /*
- * "If a cache has two fresh responses for the same representation with
- * different validators, it MUST use the one with the more recent Date
- * header."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.5
- */
@Test
public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
@@ -1857,13 +730,6 @@ public void testKeepsMostRecentDateHeaderForFreshResponse() throws Exception {
Assertions.assertEquals("\"etag1\"", result.getFirstHeader("ETag").getValue());
}
- /*
- * "HTTP/1.1 clients: - If an entity tag has been provided by the origin
- * server, MUST use that entity tag in any cache-conditional request (using
- * If- Match or If-None-Match)."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
- */
@Test
public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception {
@@ -1891,46 +757,24 @@ public void testValidationMustUseETagIfProvidedByOriginServer() throws Exception
final List allRequests = reqCapture.getAllValues();
Assertions.assertEquals(2, allRequests.size());
final ClassicHttpRequest validation = allRequests.get(1);
- boolean isConditional = false;
- final String[] conditionalHeaders = { "If-Range", "If-Modified-Since", "If-Unmodified-Since",
- "If-Match", "If-None-Match" };
-
- for (final String ch : conditionalHeaders) {
- if (validation.getFirstHeader(ch) != null) {
- isConditional = true;
- break;
+ boolean foundETag = false;
+ final Iterator it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
+ while (it.hasNext()) {
+ final HeaderElement elt = it.next();
+ if ("W/\"etag\"".equals(elt.getName())) {
+ foundETag = true;
}
}
-
- if (isConditional) {
- boolean foundETag = false;
- final Iterator it = MessageSupport.iterate(validation, HttpHeaders.IF_MATCH);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- if ("W/\"etag\"".equals(elt.getName())) {
- foundETag = true;
- }
+ final Iterator it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
+ while (it2.hasNext()) {
+ final HeaderElement elt = it2.next();
+ if ("W/\"etag\"".equals(elt.getName())) {
+ foundETag = true;
}
- final Iterator it2 = MessageSupport.iterate(validation, HttpHeaders.IF_NONE_MATCH);
- while (it2.hasNext()) {
- final HeaderElement elt = it2.next();
- if ("W/\"etag\"".equals(elt.getName())) {
- foundETag = true;
- }
- }
- Assertions.assertTrue(foundETag);
}
+ Assertions.assertTrue(foundETag);
}
- /*
- * "An HTTP/1.1 caching proxy, upon receiving a conditional request that
- * includes both a Last-Modified date and one or more entity tags as cache
- * validators, MUST NOT return a locally cached response to the client
- * unless that cached response is consistent with all of the conditional
- * header fields in the request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
- */
@Test
public void testConditionalRequestWhereNotAllValidatorsMatchCannotBeServedFromCache() throws Exception {
final Instant now = Instant.now();
@@ -1984,44 +828,23 @@ public void testConditionalRequestWhereAllValidatorsMatchMayBeServedFromCache()
Mockito.verify(mockExecChain, Mockito.atMost(2)).proceed(Mockito.any(), Mockito.any());
}
-
- /*
- * "However, a cache that does not support the Range and Content-Range
- * headers MUST NOT cache 206 (Partial Content) responses."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
- */
@Test
public void testCacheWithoutSupportForRangeAndContentRangeHeadersDoesNotCacheA206Response() throws Exception {
+ final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
+ req.setHeader("Range", "bytes=0-50");
- if (!impl.supportsRangeAndContentRangeHeaders()) {
- final ClassicHttpRequest req = new BasicClassicHttpRequest("GET", "/");
- req.setHeader("Range", "bytes=0-50");
-
- final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
- resp.setHeader("Content-Range", "bytes 0-50/128");
- resp.setHeader("ETag", "\"etag\"");
- resp.setHeader("Cache-Control", "max-age=3600");
+ final ClassicHttpResponse resp = new BasicClassicHttpResponse(206, "Partial Content");
+ resp.setHeader("Content-Range", "bytes 0-50/128");
+ resp.setHeader("ETag", "\"etag\"");
+ resp.setHeader("Cache-Control", "max-age=3600");
- Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp);
+ Mockito.when(mockExecChain.proceed(Mockito.any(),Mockito.any())).thenReturn(resp);
- execute(req);
+ execute(req);
- Mockito.verifyNoInteractions(mockCache);
- }
+ Mockito.verifyNoInteractions(mockCache);
}
- /*
- * "A response received with any other status code (e.g. status codes 302
- * and 307) MUST NOT be returned in a reply to a subsequent request unless
- * there are cache-control directives or another header(s) that explicitly
- * allow it. For example, these include the following: an Expires header
- * (section 14.21); a 'max-age', 's-maxage', 'must-revalidate',
- * 'proxy-revalidate', 'public' or 'private' cache-control directive
- * (section 14.9)."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
- */
@Test
public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() throws Exception {
originResponse = new BasicClassicHttpResponse(302, "Temporary Redirect");
@@ -2037,11 +860,6 @@ public void test302ResponseWithoutExplicitCacheabilityIsNotReturnedFromCache() t
Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /*
- * "A transparent proxy MUST NOT modify any of the following fields in a
- * request or response, and it MUST NOT add any of these fields if not
- * already present: - Content-Location - Content-MD5 - ETag - Last-Modified
- */
private void testDoesNotModifyHeaderFromOrigin(final String header, final String value) throws Exception {
originResponse = HttpTestUtils.make200Response();
originResponse.setHeader(header, value);
@@ -2255,12 +1073,6 @@ public void testDoesNotAddLastModifiedToRequestIfNotPresent() throws Exception {
testDoesNotAddHeaderToRequestIfNotPresent("Last-Modified");
}
- /* " A transparent proxy MUST NOT modify any of the following
- * fields in a response: - Expires
- * but it MAY add any of these fields if not already present. If
- * an Expires header is added, it MUST be given a field-value
- * identical to that of the Date header in that response.
- */
@Test
public void testDoesNotModifyExpiresHeaderFromOrigin() throws Exception {
final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
@@ -2288,32 +1100,6 @@ public void testExpiresHeaderMatchesDateIfAddedToOriginResponse() throws Excepti
}
}
- @Test
- public void testExpiresHeaderMatchesDateIfAddedToCacheHit() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
-
- originResponse.setHeader("Cache-Control","max-age=3600");
- originResponse.removeHeaders("Expires");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- final Header expHdr = result.getFirstHeader("Expires");
- if (expHdr != null) {
- Assertions.assertEquals(result.getFirstHeader("Date").getValue(),
- expHdr.getValue());
- }
- }
-
- /* "A proxy MUST NOT modify or add any of the following fields in
- * a message that contains the no-transform cache-control
- * directive, or in any request: - Content-Encoding - Content-Range
- * - Content-Type"
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
- */
private void testDoesNotModifyHeaderFromOriginResponseWithNoTransform(final String header, final String value) throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
originResponse.setHeader(header, value);
@@ -2370,29 +1156,6 @@ public void testDoesNotModifyContentTypeHeaderOnCachedResponseWithNoTransform()
testDoesNotModifyHeaderOnCachedResponseWithNoTransform("Content-Type","text/html;charset=utf-8");
}
- @Test
- public void testDoesNotModifyContentRangeHeaderOnCachedResponseWithNoTransform() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("If-Range","\"etag\"");
- req1.setHeader("Range","bytes=0-49");
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("If-Range","\"etag\"");
- req2.setHeader("Range","bytes=0-49");
-
- originResponse.addHeader("Cache-Control","max-age=3600, no-transform");
- originResponse.setHeader("Content-Range", "bytes 0-49/128");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- execute(req1);
- final ClassicHttpResponse result = execute(req2);
-
- Assertions.assertEquals("bytes 0-49/128",
- result.getFirstHeader("Content-Range").getValue());
-
- Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
- }
-
@Test
public void testDoesNotAddContentEncodingHeaderToOriginResponseWithNoTransformIfNotPresent() throws Exception {
originResponse.addHeader("Cache-Control","no-transform");
@@ -2461,596 +1224,161 @@ public void testDoesNotAddContentTypeHeaderToRequestIfNotPresent() throws Except
testDoesNotAddHeaderToRequestIfNotPresent("Content-Type");
}
- /* "When a cache makes a validating request to a server, and the
- * server provides a 304 (Not Modified) response or a 206 (Partial
- * Content) response, the cache then constructs a response to send
- * to the requesting client.
- *
- * If the status code is 304 (Not Modified), the cache uses the
- * entity-body stored in the cache entry as the entity-body of
- * this outgoing response.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.3
- */
- public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("ETag","\"etag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control","max-age=0, max-stale=0");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpResponse result = execute(req2);
-
- final InputStream i1 = resp1.getEntity().getContent();
- final InputStream i2 = result.getEntity().getContent();
- int b1, b2;
- while((b1 = i1.read()) != -1) {
- b2 = i2.read();
- Assertions.assertEquals(b1, b2);
- }
- b2 = i2.read();
- Assertions.assertEquals(-1, b2);
- i1.close();
- i2.close();
- }
-
- /* "The end-to-end headers stored in the cache entry are used for
- * the constructed response, except that ...
- *
- * - any end-to-end headers provided in the 304 or 206 response MUST
- * replace the corresponding headers from the cache entry.
- *
- * Unless the cache decides to remove the cache entry, it MUST
- * also replace the end-to-end headers stored with the cache entry
- * with corresponding headers received in the incoming response,
- * except for Warning headers as described immediately above."
- */
- private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
- r.setHeader("Allow","GET");
- r.setHeader("Content-Encoding","gzip");
- r.setHeader("Content-Language","en");
- r.setHeader("Content-Length", "128");
- r.setHeader("Content-Location","http://foo.example.com/other");
- r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
- r.setHeader("Content-Type", "text/html;charset=utf-8");
- r.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(10)));
- r.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now().minusSeconds(10)));
- r.setHeader("Location", "http://foo.example.com/other2");
- r.setHeader("Retry-After","180");
- }
-
@Test
- public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("ETag","\"etag\"");
- decorateWithEndToEndHeaders(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
- resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- resp2.setHeader("Server", "MockServer/1.0");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp2);
- final ClassicHttpResponse result = execute(req2);
-
- final String[] endToEndHeaders = {
- "Cache-Control", "ETag", "Allow", "Content-Encoding",
- "Content-Language", "Content-Length", "Content-Location",
- "Content-MD5", "Content-Type", "Expires", "Last-Modified",
- "Location", "Retry-After"
- };
- for(final String h : endToEndHeaders) {
- Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
- HttpTestUtils.getCanonicalHeaderValue(result, h));
- }
- }
-
- @Test
- public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
-
+ public void testCachedEntityBodyIsUsedForResponseAfter304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag","\"etag\"");
- decorateWithEndToEndHeaders(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
- resp2.setHeader("Cache-Control", "max-age=1800");
- resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
- resp2.setHeader("Server", "MockServer/1.0");
- resp2.setHeader("Allow", "GET,HEAD");
- resp2.setHeader("Content-Language", "en,en-us");
- resp2.setHeader("Content-Location", "http://foo.example.com/new");
- resp2.setHeader("Content-Type","text/html");
- resp2.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(5)));
- resp2.setHeader("Location", "http://foo.example.com/new2");
- resp2.setHeader("Retry-After","120");
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- final ClassicHttpResponse result1 = execute(req2);
- final ClassicHttpResponse result2 = execute(req3);
-
- final String[] endToEndHeaders = {
- "Date", "Cache-Control", "Allow", "Content-Language",
- "Content-Location", "Content-Type", "Expires", "Location",
- "Retry-After"
- };
- for(final String h : endToEndHeaders) {
- Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
- HttpTestUtils.getCanonicalHeaderValue(result1, h));
- Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
- HttpTestUtils.getCanonicalHeaderValue(result2, h));
- }
- }
-
- /* "If a header field-name in the incoming response matches more
- * than one header in the cache entry, all such old headers MUST
- * be replaced."
- */
- @Test
- public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.addHeader("Cache-Control","max-age=3600");
- resp1.addHeader("Cache-Control","public");
- resp1.setHeader("ETag","\"etag\"");
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
- resp2.setHeader("Cache-Control", "max-age=1800");
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpResponse result1 = execute(req2);
- final ClassicHttpResponse result2 = execute(req3);
-
- final String h = "Cache-Control";
- Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
- HttpTestUtils.getCanonicalHeaderValue(result1, h));
- Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
- HttpTestUtils.getCanonicalHeaderValue(result2, h));
- }
-
- /* "If a cache has a stored non-empty set of subranges for an
- * entity, and an incoming response transfers another subrange,
- * the cache MAY combine the new subrange with the existing set if
- * both the following conditions are met:
- *
- * - Both the incoming response and the cache entry have a cache
- * validator.
- *
- * - The two cache validators match using the strong comparison
- * function (see section 13.3.3).
- *
- * If either requirement is not met, the cache MUST use only the
- * most recent partial response (based on the Date values
- * transmitted with every response, and using the incoming
- * response if these values are equal or missing), and MUST
- * discard the other partial information."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.4
- */
- @Test
- public void testCannotCombinePartialResponseIfIncomingResponseDoesNotHaveACacheValidator() throws Exception {
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("ETag","\"etag1\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- @Test
- public void testCannotCombinePartialResponseIfCacheEntryDoesNotHaveACacheValidator() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag1\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- @Test
- public void testCannotCombinePartialResponseIfCacheValidatorsDoNotStronglyMatch() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("ETag","\"etag1\"");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag2\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
-
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- @Test
- public void testMustDiscardLeastRecentPartialResponseIfIncomingRequestDoesNotHaveCacheValidator() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("ETag","\"etag1\"");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
-
- // must make this request; cannot serve from cache
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
-
- execute(req1);
- execute(req2);
- execute(req3);
- }
-
- @Test
- public void testMustDiscardLeastRecentPartialResponseIfCachedResponseDoesNotHaveCacheValidator() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
+ final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
+ req2.setHeader("Cache-Control","max-age=0, max-stale=0");
+ final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
- final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag1\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
+ execute(req1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
+ final ClassicHttpResponse result = execute(req2);
- // must make this request; cannot serve from cache
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
+ try (final InputStream i1 = resp1.getEntity().getContent();
+ final InputStream i2 = result.getEntity().getContent()) {
+ int b1, b2;
+ while((b1 = i1.read()) != -1) {
+ b2 = i2.read();
+ Assertions.assertEquals(b1, b2);
+ }
+ b2 = i2.read();
+ Assertions.assertEquals(-1, b2);
+ }
+ }
- execute(req1);
- execute(req2);
- execute(req3);
+ private void decorateWithEndToEndHeaders(final ClassicHttpResponse r) {
+ r.setHeader("Allow","GET");
+ r.setHeader("Content-Encoding","gzip");
+ r.setHeader("Content-Language","en");
+ r.setHeader("Content-Length", "128");
+ r.setHeader("Content-Location","http://foo.example.com/other");
+ r.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+ r.setHeader("Content-Type", "text/html;charset=utf-8");
+ r.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(10)));
+ r.setHeader("Last-Modified", DateUtils.formatStandardDate(Instant.now().minusSeconds(10)));
+ r.setHeader("Location", "http://foo.example.com/other2");
+ r.setHeader("Retry-After","180");
}
@Test
- public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatch() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
-
+ public void testResponseIncludesCacheEntryEndToEndHeadersForResponseAfter304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
+ final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("Etag","\"etag1\"");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ resp1.setHeader("ETag","\"etag\"");
+ decorateWithEndToEndHeaders(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag2\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
+ final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+ resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
+ resp2.setHeader("Server", "MockServer/1.0");
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("Range","bytes=0-49");
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
+ execute(req1);
- // must make this request; cannot serve from cache
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
+ Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(req2), Mockito.any())).thenReturn(resp2);
+ final ClassicHttpResponse result = execute(req2);
- execute(req1);
- execute(req2);
- execute(req3);
+ final String[] endToEndHeaders = {
+ "Cache-Control", "ETag", "Allow", "Content-Encoding",
+ "Content-Language", "Content-Length", "Content-Location",
+ "Content-MD5", "Content-Type", "Expires", "Last-Modified",
+ "Location", "Retry-After"
+ };
+ for(final String h : endToEndHeaders) {
+ Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp1, h),
+ HttpTestUtils.getCanonicalHeaderValue(result, h));
+ }
}
@Test
- public void testMustDiscardLeastRecentPartialResponseIfCacheValidatorsDoNotStronglyMatchEvenIfResponsesOutOfOrder() throws Exception {
-
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
- final Instant twoSecondsAgo = Instant.now().plusSeconds(2);
+ public void testUpdatedEndToEndHeadersFrom304ArePassedOnResponseAndUpdatedInCacheEntry() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
+ final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("Etag","\"etag1\"");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ resp1.setHeader("ETag","\"etag\"");
+ decorateWithEndToEndHeaders(resp1);
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag2\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
+ final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+ resp2.setHeader("Cache-Control", "max-age=1800");
+ resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
+ resp2.setHeader("Server", "MockServer/1.0");
+ resp2.setHeader("Allow", "GET,HEAD");
+ resp2.setHeader("Content-Language", "en,en-us");
+ resp2.setHeader("Content-Location", "http://foo.example.com/new");
+ resp2.setHeader("Content-Type","text/html");
+ resp2.setHeader("Expires", DateUtils.formatStandardDate(Instant.now().plusSeconds(5)));
+ resp2.setHeader("Location", "http://foo.example.com/new2");
+ resp2.setHeader("Retry-After","120");
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("Range","bytes=50-127");
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
-
- // must make this request; cannot serve from cache
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
execute(req1);
- execute(req2);
- execute(req3);
- }
- @Test
- public void testMustDiscardCachedPartialResponseIfCacheValidatorsDoNotStronglyMatchAndDateHeadersAreEqual() throws Exception {
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ final ClassicHttpResponse result1 = execute(req2);
+ final ClassicHttpResponse result2 = execute(req3);
- final Instant now = Instant.now();
- final Instant oneSecondAgo = now.minusSeconds(1);
+ final String[] endToEndHeaders = {
+ "Date", "Cache-Control", "Allow", "Content-Language",
+ "Content-Location", "Content-Type", "Expires", "Location",
+ "Retry-After"
+ };
+ for(final String h : endToEndHeaders) {
+ Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
+ HttpTestUtils.getCanonicalHeaderValue(result1, h));
+ Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
+ HttpTestUtils.getCanonicalHeaderValue(result2, h));
+ }
+ }
+ @Test
+ public void testMultiHeadersAreSuccessfullyReplacedOn304Validation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
- req1.setHeader("Range","bytes=0-49");
-
- final ClassicHttpResponse resp1 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp1.setEntity(HttpTestUtils.makeBody(50));
- resp1.setHeader("Cache-Control","max-age=3600");
- resp1.setHeader("Content-Range","bytes 0-49/128");
- resp1.setHeader("Etag","\"etag1\"");
- resp1.setHeader("Server","MockServer/1.0");
- resp1.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
+ resp1.addHeader("Cache-Control","max-age=3600");
+ resp1.addHeader("Cache-Control","public");
+ resp1.setHeader("ETag","\"etag\"");
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
- req2.setHeader("Range","bytes=50-127");
+ req2.setHeader("Cache-Control", "max-age=0, max-stale=0");
+ final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+ resp2.setHeader("Cache-Control", "max-age=1800");
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_PARTIAL_CONTENT, "Partial Content");
- resp2.setEntity(HttpTestUtils.makeBody(78));
- resp2.setHeader("Cache-Control","max-age=3600");
- resp2.setHeader("Content-Range","bytes 50-127/128");
- resp2.setHeader("ETag","\"etag2\"");
- resp2.setHeader("Server","MockServer/1.0");
- resp2.setHeader("Date", DateUtils.formatStandardDate(oneSecondAgo));
+ final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
- req3.setHeader("Range","bytes=0-49");
+ execute(req1);
- final ClassicHttpResponse resp3 = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
- resp3.setEntity(HttpTestUtils.makeBody(128));
- resp3.setHeader("Server","MockServer/1.0");
- resp3.setHeader("Date", DateUtils.formatStandardDate(now));
+ Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- // must make this request; cannot serve from cache
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
+ final ClassicHttpResponse result1 = execute(req2);
+ final ClassicHttpResponse result2 = execute(req3);
- execute(req1);
- execute(req2);
- execute(req3);
+ final String h = "Cache-Control";
+ Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
+ HttpTestUtils.getCanonicalHeaderValue(result1, h));
+ Assertions.assertEquals(HttpTestUtils.getCanonicalHeaderValue(resp2, h),
+ HttpTestUtils.getCanonicalHeaderValue(result2, h));
}
- /* "When the cache receives a subsequent request whose Request-URI
- * specifies one or more cache entries including a Vary header
- * field, the cache MUST NOT use such a cache entry to construct a
- * response to the new request unless all of the selecting
- * request-headers present in the new request match the
- * corresponding stored request-headers in the original request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
@Test
public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch() throws Exception {
@@ -3064,6 +1392,8 @@ public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch()
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.removeHeaders("Accept-Encoding");
@@ -3074,16 +1404,11 @@ public void testCannotUseVariantCacheEntryIfNotAllSelectingRequestHeadersMatch()
// not allowed to have a cache hit; must forward request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
+
+ Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /* "A Vary header field-value of "*" always fails to match and
- * subsequent requests on that resource can only be properly
- * interpreted by the origin server."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
@Test
public void testCannotServeFromCacheForVaryStar() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3095,6 +1420,8 @@ public void testCannotServeFromCacheForVaryStar() throws Exception {
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -3104,43 +1431,13 @@ public void testCannotServeFromCacheForVaryStar() throws Exception {
// not allowed to have a cache hit; must forward request
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
+
+ Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
- /* " If the selecting request header fields for the cached entry
- * do not match the selecting request header fields of the new
- * request, then the cache MUST NOT use a cached entry to satisfy
- * the request unless it first relays the new request to the
- * origin server in a conditional request and the server responds
- * with 304 (Not Modified), including an entity tag or
- * Content-Location that indicates the entity to be used.
- *
- * If an entity tag was assigned to a cached representation, the
- * forwarded request SHOULD be conditional and include the entity
- * tags in an If-None-Match header field from all its cache
- * entries for the resource. This conveys to the server the set of
- * entities currently held by the cache, so that if any one of
- * these entities matches the requested entity, the server can use
- * the ETag header field in its 304 (Not Modified) response to
- * tell the cache which entry is appropriate. If the entity-tag of
- * the new response matches that of an existing entry, the new
- * response SHOULD be used to processChallenge the header fields of the
- * existing entry, and the result MUST be returned to the client.
- *
- * NOTE: Tests that a non-matching variant cannot be served from cache unless conditionally validated.
- *
- * The original test expected the response to have an ETag header with a specific value, but the changes made
- * to the cache implementation made it so that ETag headers are not added to variant responses. Therefore, the test
- * was updated to expect that the variant response has a Vary header instead, indicating that the response may vary
- * based on the User-Agent header. Additionally, the mock response for the second request was changed to include a Vary
- * header to match the first response. This ensures that the second request will not match the first response in the
- * cache and will have to be validated conditionally against the origin server.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
- */
- @Test
- public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
+ @Test
+ public void testNonMatchingVariantCannotBeServedFromCacheUnlessConditionallyValidated() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
req1.setHeader("User-Agent","MyBrowser/1.0");
@@ -3173,16 +1470,6 @@ public void testNonmatchingVariantCannotBeServedFromCacheUnlessConditionallyVali
Assertions.assertTrue(HttpTestUtils.semanticallyTransparent(resp200, result));
}
- /* "Some HTTP methods MUST cause a cache to invalidate an
- * entity. This is either the entity referred to by the
- * Request-URI, or by the Location or Content-Location headers (if
- * present). These methods are:
- * - PUT
- * - DELETE
- * - POST
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9
- */
protected void testUnsafeOperationInvalidatesCacheForThatUri(
final ClassicHttpRequest unsafeReq) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3191,10 +1478,14 @@ protected void testUnsafeOperationInvalidatesCacheForThatUri(
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ execute(unsafeReq);
+
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("Cache-Control","public, max-age=3600");
@@ -3202,8 +1493,6 @@ protected void testUnsafeOperationInvalidatesCacheForThatUri(
// this origin request MUST happen due to invalidation
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
- execute(req1);
- execute(unsafeReq);
execute(req3);
}
@@ -3241,10 +1530,14 @@ protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
+ execute(unsafeReq);
+
final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
final ClassicHttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("Cache-Control","public, max-age=3600");
@@ -3252,8 +1545,6 @@ protected void testUnsafeMethodInvalidatesCacheForHeaderUri(
// this origin request MUST happen due to invalidation
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp3);
- execute(req1);
- execute(unsafeReq);
execute(req3);
}
@@ -3329,92 +1620,6 @@ public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() thro
testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req);
}
- /* "In order to prevent denial of service attacks, an invalidation based on the URI
- * in a Location or Content-Location header MUST only be performed if the host part
- * is the same as in the Request-URI."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10
- */
- protected void testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(
- final ClassicHttpRequest unsafeReq) throws Exception {
-
- final HttpHost otherHost = new HttpHost("bar.example.com", 80);
- final HttpRoute otherRoute = new HttpRoute(otherHost);
- final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/content");
- final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
- resp1.setHeader("Cache-Control","public, max-age=3600");
-
- final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
-
- final ClassicHttpRequest req3 = new BasicClassicHttpRequest("GET", "/content");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
-
- execute(req1);
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
-
- execute(unsafeReq);
- execute(req3);
- }
-
- protected void testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(
- final ClassicHttpRequest unsafeReq) throws Exception {
- unsafeReq.setHeader("Content-Location","http://bar.example.com/content");
- testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
- }
-
- protected void testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(
- final ClassicHttpRequest unsafeReq) throws Exception {
- unsafeReq.setHeader("Location","http://bar.example.com/content");
- testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq);
- }
-
- @Test
- public void testPutDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
- }
-
- @Test
- public void testPutDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = makeRequestWithBody("PUT","/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
- }
-
- @Test
- public void testPostDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = makeRequestWithBody("POST","/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
- }
-
- @Test
- public void testPostDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = makeRequestWithBody("POST","/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
- }
-
- @Test
- public void testDeleteDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req);
- }
-
- @Test
- public void testDeleteDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception {
- final ClassicHttpRequest req = new BasicClassicHttpRequest("DELETE", "/");
- testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req);
- }
-
- /* "All methods that might be expected to cause modifications to the origin
- * server's resources MUST be written through to the origin server. This
- * currently includes all methods except for GET and HEAD. A cache MUST NOT
- * reply to such a request from a client before having transmitted the
- * request to the inbound server, and having received a corresponding
- * response from the inbound server."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.11
- */
private void testRequestIsWrittenThroughToOrigin(final ClassicHttpRequest req) throws Exception {
final ClassicHttpResponse resp = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content");
final ClassicHttpRequest wrapper = req;
@@ -3469,13 +1674,6 @@ public void testUnknownMethodRequestsAreWrittenThroughToOrigin() throws Exceptio
testRequestIsWrittenThroughToOrigin(req);
}
- /* "If a cache receives a value larger than the largest positive
- * integer it can represent, or if any of its age calculations
- * overflows, it MUST transmit an Age header with a value of
- * 2147483648 (2^31)."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6
- */
@Test
public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
final String reallyOldAge = "1" + Long.MAX_VALUE;
@@ -3489,12 +1687,6 @@ public void testTransmitsAgeHeaderIfIncomingAgeHeaderTooBig() throws Exception {
result.getFirstHeader("Age").getValue());
}
- /* "A proxy MUST NOT modify the Allow header field even if it does not
- * understand all the methods specified, since the user agent might
- * have other means of communicating with the origin server.
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
- */
@Test
public void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
final String allowHeaderValue = "GET, HEAD, FOOBAR";
@@ -3505,31 +1697,6 @@ public void testDoesNotModifyAllowHeaderWithUnknownMethods() throws Exception {
HttpTestUtils.getCanonicalHeaderValue(result, "Allow"));
}
- /* "When a shared cache (see section 13.7) receives a request
- * containing an Authorization field, it MUST NOT return the
- * corresponding response as a reply to any other request, unless one
- * of the following specific exceptions holds:
- *
- * 1. If the response includes the "s-maxage" cache-control
- * directive, the cache MAY use that response in replying to a
- * subsequent request. But (if the specified maximum age has
- * passed) a proxy cache MUST first revalidate it with the origin
- * server, using the request-headers from the new request to allow
- * the origin server to authenticate the new request. (This is the
- * defined behavior for s-maxage.) If the response includes "s-
- * maxage=0", the proxy MUST always revalidate it before re-using
- * it.
- *
- * 2. If the response includes the "must-revalidate" cache-control
- * directive, the cache MAY use that response in replying to a
- * subsequent request. But if the response is stale, all caches
- * MUST first revalidate it with the origin server, using the
- * request-headers from the new request to allow the origin server
- * to authenticate the new request.
- *
- * 3. If the response includes the "public" cache-control directive,
- * it MAY be returned in reply to any subsequent request.
- */
protected void testSharedCacheRevalidatesAuthorizedResponse(
final ClassicHttpResponse authorizedResponse, final int minTimes, final int maxTimes) throws Exception {
if (config.isSharedCache()) {
@@ -3651,11 +1818,6 @@ public void testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedRes
testSharedCacheMustUseNewRequestHeadersWhenRevalidatingAuthorizedResponse(resp1);
}
- /* "The request includes a "no-cache" cache-control directive...
- * The server MUST NOT use a cached copy when responding to such a request."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
- */
protected void testCacheIsNotUsedWhenRespondingToRequest(final ClassicHttpRequest req) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp1 = HttpTestUtils.make200Response();
@@ -3690,15 +1852,6 @@ public void testCacheIsNotUsedWhenRespondingToRequestWithCacheControlNoCache() t
testCacheIsNotUsedWhenRespondingToRequest(req);
}
- /* "When the must-revalidate directive is present in a response received
- * by a cache, that cache MUST NOT use the entry after it becomes stale
- * to respond to a subsequent request without first revalidating it with
- * the origin server. (I.e., the cache MUST do an end-to-end
- * revalidation every time, if, based solely on the origin server's
- * Expires or max-age value, the cached response is stale.)"
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
- */
protected void testStaleCacheResponseMustBeRevalidatedWithOrigin(
final ClassicHttpResponse staleResponse) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3746,11 +1899,6 @@ public void testStaleEntryWithMustRevalidateIsNotUsedWithoutRevalidatingWithOrig
testStaleCacheResponseMustBeRevalidatedWithOrigin(response);
}
-
- /* "In all circumstances an HTTP/1.1 cache MUST obey the must-revalidate
- * directive; in particular, if the cache cannot reach the origin server
- * for any reason, it MUST generate a 504 (Gateway Timeout) response."
- */
protected void testGenerates504IfCannotRevalidateStaleResponse(
final ClassicHttpResponse staleResponse) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3781,12 +1929,6 @@ public void testGenerates504IfCannotRevalidateAMustRevalidateEntry() throws Exce
testGenerates504IfCannotRevalidateStaleResponse(resp1);
}
- /* "The proxy-revalidate directive has the same meaning as the must-
- * revalidate directive, except that it does not apply to non-shared
- * user agent caches."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
- */
@Test
public void testStaleEntryWithProxyRevalidateOnSharedCacheIsNotUsedWithoutRevalidatingWithOrigin() throws Exception {
if (config.isSharedCache()) {
@@ -3815,12 +1957,6 @@ public void testGenerates504IfSharedCacheCannotRevalidateAProxyRevalidateEntry()
}
}
- /* "[The cache control directive] "private" Indicates that all or part of
- * the response message is intended for a single user and MUST NOT be
- * cached by a shared cache."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
- */
@Test
public void testCacheControlPrivateIsNotCacheableBySharedCache() throws Exception {
if (config.isSharedCache()) {
@@ -3865,14 +2001,6 @@ public void testCacheControlPrivateOnFieldIsNotReturnedBySharedCache() throws Ex
}
}
- /* "If the no-cache directive does not specify a field-name, then a
- * cache MUST NOT use the response to satisfy a subsequent request
- * without successful revalidation with the origin server. This allows
- * an origin server to prevent caching even by caches that have been
- * configured to return stale responses to client requests."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
- */
@Test
public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3882,14 +2010,17 @@ public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidation() thro
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
// this MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
+
+ Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any());
}
@Test
@@ -3901,6 +2032,8 @@ public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWit
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
req2.setHeader("Cache-Control","max-stale=7200");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -3908,16 +2041,9 @@ public void testNoCacheCannotSatisfyASubsequentRequestWithoutRevalidationEvenWit
// this MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
}
- /* "If the no-cache directive does specify one or more field-names, then
- * a cache MAY use the response to satisfy a subsequent request, subject
- * to any other restrictions on caching. However, the specified
- * field-name(s) MUST NOT be sent in the response to a subsequent request
- * without successful revalidation with the origin server."
- */
@Test
public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -3928,6 +2054,8 @@ public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag","\"etag\"");
@@ -3936,7 +2064,6 @@ public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exceptio
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
final ClassicHttpResponse result = execute(req2);
final ArgumentCaptor reqCapture = ArgumentCaptor.forClass(ClassicHttpRequest.class);
@@ -3948,21 +2075,6 @@ public void testNoCacheOnFieldIsNotReturnedWithoutRevalidation() throws Exceptio
}
}
- /* "The purpose of the no-store directive is to prevent the inadvertent
- * release or retention of sensitive information (for example, on backup
- * tapes). The no-store directive applies to the entire message, and MAY
- * be sent either in a response or in a request. If sent in a request, a
- * cache MUST NOT store any part of either this request or any response
- * to it. If sent in a response, a cache MUST NOT store any part of
- * either this response or the request that elicited it. This directive
- * applies to both non- shared and shared caches. "MUST NOT store" in
- * this context means that the cache MUST NOT intentionally store the
- * information in non-volatile storage, and MUST make a best-effort
- * attempt to remove the information from volatile storage as promptly
- * as possible after forwarding it."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
- */
@Test
public void testNoStoreOnRequestIsNotStoredInCache() throws Exception {
request.setHeader("Cache-Control","no-store");
@@ -4004,11 +2116,6 @@ public void testNoStoreOnResponseIsNotStoredInCacheEvenWithContraryIndicators()
Mockito.verifyNoInteractions(mockCache);
}
- /* "If multiple encodings have been applied to an entity, the content
- * codings MUST be listed in the order in which they were applied."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
- */
@Test
public void testOrderOfMultipleContentEncodingHeaderValuesIsPreserved() throws Exception {
originResponse.addHeader("Content-Encoding","gzip");
@@ -4060,12 +2167,6 @@ public void testOrderOfMultipleParametersInContentEncodingHeaderIsPreserved() th
Assertions.assertEquals(2, total_encodings);
}
- /* "A cache cannot assume that an entity with a Content-Location
- * different from the URI used to retrieve it can be used to respond
- * to later requests on that Content-Location URI."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.14
- */
@Test
public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheableResource() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/foo");
@@ -4074,6 +2175,8 @@ public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheable
resp1.setHeader("Etag","\"etag\"");
resp1.setHeader("Content-Location","http://foo.example.com/bar");
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/bar");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","public,max-age=3600");
@@ -4082,14 +2185,9 @@ public void testCacheDoesNotAssumeContentLocationHeaderIndicatesAnotherCacheable
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
}
- /* "A received message that does not have a Date header field MUST be
- * assigned one by the recipient if the message will be cached by that
- * recipient or gatewayed via a protocol which requires a Date."
- */
@Test
public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throws Exception {
originResponse.removeHeaders("Date");
@@ -4102,13 +2200,6 @@ public void testCachedResponsesWithMissingDateHeadersShouldBeAssignedOne() throw
Assertions.assertNotNull(result.getFirstHeader("Date"));
}
- /* "The Expires entity-header field gives the date/time after which the
- * response is considered stale.... HTTP/1.1 clients and caches MUST
- * treat other invalid date formats, especially including the value '0',
- * as in the past (i.e., 'already expired')."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
- */
private void testInvalidExpiresHeaderIsTreatedAsStale(
final String expiresHeader) throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -4117,6 +2208,8 @@ private void testInvalidExpiresHeaderIsTreatedAsStale(
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Expires", expiresHeader);
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -4124,7 +2217,6 @@ private void testInvalidExpiresHeaderIsTreatedAsStale(
// second request to origin MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
}
@@ -4138,11 +2230,6 @@ public void testExpiresZeroHeaderIsTreatedAsStale() throws Exception {
testInvalidExpiresHeaderIsTreatedAsStale("0");
}
- /* "To mark a response as 'already expired,' an origin server sends
- * an Expires date that is equal to the Date header value."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
- */
@Test
public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exception {
final ClassicHttpRequest req1 = new BasicClassicHttpRequest("GET", "/");
@@ -4151,6 +2238,8 @@ public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exceptio
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Expires", resp1.getFirstHeader("Date").getValue());
+ execute(req1);
+
final ClassicHttpRequest req2 = new BasicClassicHttpRequest("GET", "/");
final ClassicHttpResponse resp2 = HttpTestUtils.make200Response();
@@ -4158,15 +2247,9 @@ public void testExpiresHeaderEqualToDateHeaderIsTreatedAsStale() throws Exceptio
// second request to origin MUST happen
Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
- execute(req1);
execute(req2);
}
- /* "If the response is being forwarded through a proxy, the proxy
- * application MUST NOT modify the Server response-header."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.38
- */
@Test
public void testDoesNotModifyServerResponseHeader() throws Exception {
final String server = "MockServer/1.0";
@@ -4178,38 +2261,6 @@ public void testDoesNotModifyServerResponseHeader() throws Exception {
Assertions.assertEquals(server, result.getFirstHeader("Server").getValue());
}
- /* "A Vary field value of '*' signals that unspecified parameters
- * not limited to the request-headers (e.g., the network address
- * of the client), play a role in the selection of the response
- * representation. The '*' value MUST NOT be generated by a proxy
- * server; it may only be generated by an origin server."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
- */
- @Test
- public void testVaryStarIsNotGeneratedByProxy() throws Exception {
- request.setHeader("User-Agent","my-agent/1.0");
- originResponse.setHeader("Cache-Control","public, max-age=3600");
- originResponse.setHeader("Vary","User-Agent");
- originResponse.setHeader("ETag","\"etag\"");
-
- Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
-
- final ClassicHttpResponse result = execute(request);
- final Iterator it = MessageSupport.iterate(result, HttpHeaders.VARY);
- while (it.hasNext()) {
- final HeaderElement elt = it.next();
- Assertions.assertNotEquals("*", elt.getName());
- }
- }
-
- /* "The Via general-header field MUST be used by gateways and proxies
- * to indicate the intermediate protocols and recipients between the
- * user agent and the server on requests, and between the origin server
- * and the client on responses."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
- */
@Test
public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
request.removeHeaders(HttpHeaders.VIA);
@@ -4233,7 +2284,6 @@ public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception
assertValidViaHeader(result.getFirstHeader(HttpHeaders.VIA).getValue());
}
-
private void assertValidViaHeader(final String via) {
// Via = HttpHeaders.VIA ":" 1#( received-protocol received-by [ comment ] )
// received-protocol = [ protocol-name "/" ] protocol-version
@@ -4291,17 +2341,6 @@ private boolean isValidComment(final String s) {
return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
}
-
- /*
- * "The received-protocol indicates the protocol version of the message
- * received by the server or client along each segment of the request/
- * response chain. The received-protocol version is appended to the Via
- * field value when the message is forwarded so that information about
- * the protocol capabilities of upstream applications remains visible
- * to all recipients."
- *
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
- */
@Test
public void testViaHeaderOnRequestProperlyRecordsClientProtocol() throws Exception {
final ClassicHttpRequest originalRequest = new BasicClassicHttpRequest("GET", "/");