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", "/");