hc-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ol...@apache.org
Subject svn commit: r939814 [5/6] - in /httpcomponents/httpclient/trunk: ./ httpclient-cache/ httpclient-cache/src/ httpclient-cache/src/main/ httpclient-cache/src/main/java/ httpclient-cache/src/main/java/org/ httpclient-cache/src/main/java/org/apache/ httpcl...
Date Fri, 30 Apr 2010 21:00:10 GMT
Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java?rev=939814&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/client/cache/impl/TestProtocolRequirements.java Fri Apr 30 21:00:08 2010
@@ -0,0 +1,3147 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.client.cache.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.Random;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.cache.HttpCache;
+import org.apache.http.client.cache.impl.BasicHttpCache;
+import org.apache.http.client.cache.impl.CacheEntry;
+import org.apache.http.client.cache.impl.CachingHttpClient;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.RequestWrapper;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.protocol.HttpContext;
+import org.easymock.Capture;
+import org.easymock.IExpectationSetters;
+import org.easymock.classextension.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 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.
+ */
+public class TestProtocolRequirements {
+
+    private static ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP", 1, 1);
+
+    private static int MAX_BYTES = 1024;
+    private static int MAX_ENTRIES = 100;
+    private int entityLength = 128;
+
+    private HttpHost host;
+    private HttpEntity body;
+    private HttpEntity mockEntity;
+    private HttpClient mockBackend;
+    private HttpCache<CacheEntry> mockCache;
+    private HttpRequest request;
+    private HttpResponse originResponse;
+
+    private CachingHttpClient impl;
+
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setUp() {
+        host = new HttpHost("foo.example.com");
+
+        body = makeBody(entityLength);
+
+        request = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+        originResponse = make200Response();
+
+        HttpCache<CacheEntry> cache = new BasicHttpCache(MAX_ENTRIES);
+        mockBackend = EasyMock.createMock(HttpClient.class);
+        mockEntity = EasyMock.createMock(HttpEntity.class);
+        mockCache = EasyMock.createMock(HttpCache.class);
+        impl = new CachingHttpClient(mockBackend, cache, MAX_BYTES);
+    }
+
+    private void replayMocks() {
+        EasyMock.replay(mockBackend);
+        EasyMock.replay(mockCache);
+        EasyMock.replay(mockEntity);
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mockBackend);
+        EasyMock.verify(mockCache);
+        EasyMock.verify(mockEntity);
+    }
+
+    private HttpResponse make200Response() {
+        HttpResponse out = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_OK, "OK");
+        out.setHeader("Date", DateUtils.formatDate(new Date()));
+        out.setHeader("Server", "MockOrigin/1.0");
+        out.setHeader("Content-Length", "128");
+        out.setEntity(makeBody(128));
+        return out;
+    }
+
+    private HttpEntity makeBody(int nbytes) {
+        byte[] bytes = new byte[nbytes];
+        (new Random()).nextBytes(bytes);
+        return new ByteArrayEntity(bytes);
+    }
+
+    private IExpectationSetters<HttpResponse> backendExpectsAnyRequest() throws Exception {
+        HttpResponse resp = mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
+                .isA(HttpRequest.class), (HttpContext) EasyMock.isNull());
+        return EasyMock.expect(resp);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void emptyMockCacheExpectsNoPuts() throws Exception {
+        mockBackend = EasyMock.createMock(HttpClient.class);
+        mockCache = EasyMock.createMock(HttpCache.class);
+        mockEntity = EasyMock.createMock(HttpEntity.class);
+
+        impl = new CachingHttpClient(mockBackend, mockCache, MAX_BYTES);
+
+        EasyMock.expect(mockCache.getEntry((String) EasyMock.anyObject())).andReturn(null)
+                .anyTimes();
+
+        mockCache.removeEntry(EasyMock.isA(String.class));
+        EasyMock.expectLastCall().anyTimes();
+    }
+
+    public static HttpRequest eqRequest(HttpRequest in) {
+        EasyMock.reportMatcher(new RequestEquivalent(in));
+        return null;
+    }
+
+    @Test
+    public void testCacheMissOnGETUsesOriginResponse() throws Exception {
+        EasyMock.expect(mockBackend.execute(host, request, (HttpContext) null)).andReturn(
+                originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.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 BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 2, 13));
+
+        EasyMock.expect(mockBackend.execute(host, request, (HttpContext) null)).andReturn(
+                originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.assertSame(originResponse, result);
+    }
+
+    @Test
+    public void testHigher1_XProtocolVersionsDowngradeTo1_1() throws Exception {
+
+        request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2));
+
+        HttpRequest downgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(downgraded),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.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 BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+        HttpRequest upgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(upgraded), (HttpContext) EasyMock
+                        .isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.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 BasicHttpResponse(new ProtocolVersion("HTTP", 1, 2), HttpStatus.SC_OK,
+                "OK");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        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
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.assertEquals(HTTP_1_1, result.getProtocolVersion());
+    }
+
+    @Test
+    public void testResponseToA1_0RequestShouldUse1_1() throws Exception {
+        request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.assertEquals(HTTP_1_1, result.getProtocolVersion());
+    }
+
+    /*
+     * "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 BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 2));
+        request.removeHeaders("Connection");
+        request.addHeader("X-Unknown-Header", "some-value");
+
+        HttpRequest downgraded = new BasicHttpRequest("GET", "/foo", HTTP_1_1);
+        downgraded.removeHeaders("Connection");
+        downgraded.addHeader("X-Unknown-Header", "some-value");
+
+        RequestWrapper downgradedWrapper = new RequestWrapper(downgraded);
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), eqRequest(downgradedWrapper),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+    }
+
+    /*
+     * "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");
+
+        request = new BasicHttpRequest("GET", "/foo", new ProtocolVersion("HTTP", 1, 0));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.assertNull(result.getFirstHeader("TE"));
+        Assert.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(String h, HttpRequest request)
+            throws Exception {
+        Capture<HttpRequest> reqCapture = new Capture<HttpRequest>();
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.capture(reqCapture),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+
+        HttpRequest forwarded = reqCapture.getValue();
+        Assert.assertNotNull(forwarded);
+        Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(request, h), HttpTestUtils
+                .getCanonicalHeaderValue(forwarded, h));
+
+    }
+
+    @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 {
+        BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
+                HTTP_1_1);
+        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 {
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+        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 {
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+        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 {
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+        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 testOrderOfMultiplePragmaHeadersIsPreservedOnRequests() throws Exception {
+        request.addHeader("Pragma", "no-cache");
+        request.addHeader("Pragma", "x-pragma-1, x-pragma-2");
+        testOrderOfMultipleHeadersIsPreservedOnRequests("Pragma", request);
+    }
+
+    @Test
+    public void testOrderOfMultipleViaHeadersIsPreservedOnRequests() throws Exception {
+        request.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
+        request.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
+        testOrderOfMultipleHeadersIsPreservedOnRequests("Via", request);
+    }
+
+    @Test
+    public void testOrderOfMultipleWarningHeadersIsPreservedOnRequests() throws Exception {
+        request.addHeader("Warning", "199 fred \"bargle\"");
+        request.addHeader("Warning", "199 barney \"bungle\"");
+        testOrderOfMultipleHeadersIsPreservedOnRequests("Warning", request);
+    }
+
+    private void testOrderOfMultipleHeadersIsPreservedOnResponses(String h) throws Exception {
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.assertNotNull(result);
+        Assert.assertEquals(HttpTestUtils.getCanonicalHeaderValue(originResponse, h), HttpTestUtils
+                .getCanonicalHeaderValue(result, h));
+
+    }
+
+    @Test
+    public void testOrderOfMultipleAllowHeadersIsPreservedOnResponses() throws Exception {
+        originResponse = new BasicHttpResponse(HTTP_1_1, 405, "Method Not Allowed");
+        originResponse.addHeader("Allow", "HEAD");
+        originResponse.addHeader("Allow", "DELETE");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Allow");
+    }
+
+    @Test
+    public void testOrderOfMultipleCacheControlHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("Cache-Control", "max-age=0");
+        originResponse.addHeader("Cache-Control", "no-store, must-revalidate");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Cache-Control");
+    }
+
+    @Test
+    public void testOrderOfMultipleContentEncodingHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("Content-Encoding", "gzip");
+        originResponse.addHeader("Content-Encoding", "compress");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Encoding");
+    }
+
+    @Test
+    public void testOrderOfMultipleContentLanguageHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("Content-Language", "mi");
+        originResponse.addHeader("Content-Language", "en");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Content-Language");
+    }
+
+    @Test
+    public void testOrderOfMultiplePragmaHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("Pragma", "no-cache, x-pragma-2");
+        originResponse.addHeader("Pragma", "x-pragma-1");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
+    }
+
+    @Test
+    public void testOrderOfMultipleViaHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)");
+        originResponse.addHeader("Via", "1.0 ricky, 1.1 mertz, 1.0 lucy");
+        testOrderOfMultipleHeadersIsPreservedOnResponses("Pragma");
+    }
+
+    @Test
+    public void testOrderOfMultipleWWWAuthenticateHeadersIsPreservedOnResponses() throws Exception {
+        originResponse.addHeader("WWW-Authenticate", "x-challenge-1");
+        originResponse.addHeader("WWW-Authenticate", "x-challenge-2");
+        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(int code) throws Exception {
+
+        emptyMockCacheExpectsNoPuts();
+
+        originResponse = new BasicHttpResponse(HTTP_1_1, code, "Moo");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        originResponse.setHeader("Server", "MockOrigin/1.0");
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+        originResponse.setEntity(body);
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, request);
+
+        // in particular, there were no storage calls on the cache
+        verifyMocks();
+    }
+
+    @Test
+    public void testUnknownResponseStatusCodesAreNotCached() throws Exception {
+        for (int i = 102; i <= 199; i++) {
+            testUnknownResponseStatusCodeIsNotCached(i);
+        }
+        for (int i = 207; i <= 299; i++) {
+            testUnknownResponseStatusCodeIsNotCached(i);
+        }
+        for (int i = 308; i <= 399; i++) {
+            testUnknownResponseStatusCodeIsNotCached(i);
+        }
+        for (int i = 418; i <= 499; i++) {
+            testUnknownResponseStatusCodeIsNotCached(i);
+        }
+        for (int i = 506; i <= 999; i++) {
+            testUnknownResponseStatusCodeIsNotCached(i);
+        }
+    }
+
+    /*
+     * "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");
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.anyObject())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+        HttpRequest forwarded = reqCap.getValue();
+        Header[] hdrs = forwarded.getHeaders("X-Unknown-Header");
+        Assert.assertEquals(1, hdrs.length);
+        Assert.assertEquals("blahblah", hdrs[0].getValue());
+    }
+
+    @Test
+    public void testUnknownHeadersOnResponsesAreForwarded() throws Exception {
+        originResponse.addHeader("X-Unknown-Header", "blahblah");
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Header[] hdrs = result.getHeaders("X-Unknown-Header");
+        Assert.assertEquals(1, hdrs.length);
+        Assert.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 {
+        BasicHttpEntityEnclosingRequest post = EasyMock.createMockBuilder(
+                BasicHttpEntityEnclosingRequest.class).withConstructor("POST", "/", HTTP_1_1)
+                .addMockedMethods("expectContinue").createMock();
+        post.setEntity(mockEntity);
+        post.setHeader("Content-Length", "128");
+
+        Capture<HttpEntityEnclosingRequest> reqCap = new Capture<HttpEntityEnclosingRequest>();
+
+        EasyMock.expect(post.expectContinue()).andReturn(true).anyTimes();
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        EasyMock.replay(post);
+
+        impl.execute(host, post);
+
+        verifyMocks();
+        EasyMock.verify(post);
+
+        HttpEntityEnclosingRequest forwarded = reqCap.getValue();
+        Assert.assertTrue(forwarded.expectContinue());
+        boolean foundExpect = false;
+        for (Header h : forwarded.getHeaders("Expect")) {
+            for (HeaderElement elt : h.getElements()) {
+                if ("100-continue".equalsIgnoreCase(elt.getName())) {
+                    foundExpect = true;
+                    break;
+                }
+            }
+        }
+        Assert.assertTrue(foundExpect);
+    }
+
+    /*
+     * "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 {
+        BasicHttpEntityEnclosingRequest post = EasyMock.createMockBuilder(
+                BasicHttpEntityEnclosingRequest.class).withConstructor("POST", "/", HTTP_1_1)
+                .addMockedMethods("expectContinue").createMock();
+        post.setEntity(mockEntity);
+        post.setHeader("Content-Length", "128");
+
+        Capture<HttpEntityEnclosingRequest> reqCap = new Capture<HttpEntityEnclosingRequest>();
+
+        EasyMock.expect(post.expectContinue()).andReturn(false).anyTimes();
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        EasyMock.replay(post);
+
+        impl.execute(host, post);
+
+        verifyMocks();
+        EasyMock.verify(post);
+
+        HttpEntityEnclosingRequest forwarded = reqCap.getValue();
+        Assert.assertFalse(forwarded.expectContinue());
+        boolean foundExpect = false;
+        for (Header h : forwarded.getHeaders("Expect")) {
+            for (HeaderElement elt : h.getElements()) {
+                if ("100-continue".equalsIgnoreCase(elt.getName())) {
+                    foundExpect = true;
+                    break;
+                }
+            }
+        }
+        Assert.assertFalse(foundExpect);
+    }
+
+    /*
+     * "A client MUST NOT send an Expect request-header field (section 14.20)
+     * with the '100-continue' expectation if it does not intend to send a
+     * request body."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+     */
+    @Test
+    public void testExpect100ContinueIsNotSentIfThereIsNoRequestBody() throws Exception {
+        request.addHeader("Expect", "100-continue");
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+        HttpRequest forwarded = reqCap.getValue();
+        boolean foundExpectContinue = false;
+
+        for (Header h : forwarded.getHeaders("Expect")) {
+            for (HeaderElement elt : h.getElements()) {
+                if ("100-continue".equalsIgnoreCase(elt.getName())) {
+                    foundExpectContinue = true;
+                    break;
+                }
+            }
+        }
+        Assert.assertFalse(foundExpectContinue);
+    }
+
+    /*
+     * "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.
+    }
+
+    /*
+     * "A proxy MUST NOT forward a 100 (Continue) response if the request
+     * message was received from an HTTP/1.0 (or earlier) client and did not
+     * include an Expect request-header field with the '100-continue'
+     * expectation. This requirement overrides the general rule for forwarding
+     * of 1xx responses (see section 10.1)."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
+     */
+    @Test
+    public void test100ContinueResponsesAreNotForwardedTo1_0ClientsWhoDidNotAskForThem()
+            throws Exception {
+
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                new ProtocolVersion("HTTP", 1, 0));
+        post.setEntity(body);
+        post.setHeader("Content-Length", "128");
+
+        originResponse = new BasicHttpResponse(HTTP_1_1, 100, "Continue");
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        try {
+            // if a 100 response gets up to us from the HttpClient
+            // backend, we can't really handle it at that point
+            impl.execute(host, post);
+            Assert.fail("should have thrown an exception");
+        } catch (ClientProtocolException expected) {
+        }
+
+        verifyMocks();
+    }
+
+    /*
+     * "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 {
+        emptyMockCacheExpectsNoPuts();
+        request = new BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+    }
+
+    /*
+     * "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 BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+        originResponse.setEntity(null);
+        originResponse.setHeader("Content-Length", "0");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Header contentLength = result.getFirstHeader("Content-Length");
+        Assert.assertNotNull(contentLength);
+        Assert.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 BasicHttpRequest("OPTIONS", "*", HTTP_1_1);
+        request.setHeader("Max-Forwards", "0");
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+    }
+
+    /*
+     * "If the Max-Forwards field-value is an integer greater than zero, the
+     * proxy MUST decrement the field-value when it forwards the request."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
+     */
+    @Test
+    public void testDecrementsMaxForwardsWhenForwardingOPTIONSRequest() throws Exception {
+
+        request = new BasicHttpRequest("OPTIONS", "*", HTTP_1_1);
+        request.setHeader("Max-Forwards", "7");
+
+        Capture<HttpRequest> cap = new Capture<HttpRequest>();
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(cap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+
+        HttpRequest captured = cap.getValue();
+        Assert.assertEquals("6", captured.getFirstHeader("Max-Forwards").getValue());
+    }
+
+    /*
+     * "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 BasicHttpRequest("OPTIONS", "/", HTTP_1_1);
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+
+        HttpRequest forwarded = reqCap.getValue();
+        Assert.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 BasicHttpRequest("HEAD", "/", HTTP_1_1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.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(String eHeader,
+            String oldVal, String newVal) throws Exception {
+
+        // put something cacheable in the cache
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.addHeader("Cache-Control", "max-age=3600");
+        resp1.setHeader(eHeader, oldVal);
+
+        // get a head that penetrates the cache
+        HttpRequest req2 = new BasicHttpRequest("HEAD", "/", HTTP_1_1);
+        req2.addHeader("Cache-Control", "no-cache");
+        HttpResponse resp2 = make200Response();
+        resp2.setEntity(null);
+        resp2.setHeader(eHeader, newVal);
+
+        // next request doesn't tolerate stale entry
+        HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req3.addHeader("Cache-Control", "max-stale=0");
+        HttpResponse resp3 = make200Response();
+        resp3.setHeader(eHeader, newVal);
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(req1), (HttpContext) EasyMock
+                        .isNull())).andReturn(resp1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(req2), (HttpContext) EasyMock
+                        .isNull())).andReturn(resp2);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp3);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+        impl.execute(host, req3);
+
+        verifyMocks();
+    }
+
+    @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 {
+        Date now = new Date();
+        Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+        Date sixSecondsAgo = new Date(now.getTime() - 6 * 1000L);
+        testHEADResponseWithUpdatedEntityFieldsMakeACacheEntryStale("Last-Modified", DateUtils
+                .formatDate(tenSecondsAgo), DateUtils.formatDate(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 {
+        emptyMockCacheExpectsNoPuts();
+
+        BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/",
+                HTTP_1_1);
+        post.setHeader("Content-Length", "128");
+        post.setEntity(makeBody(128));
+
+        originResponse.removeHeaders("Cache-Control");
+        originResponse.removeHeaders("Expires");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, post);
+
+        verifyMocks();
+    }
+
+    /*
+     * "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 {
+        emptyMockCacheExpectsNoPuts();
+
+        BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/",
+                HTTP_1_1);
+        put.setEntity(makeBody(128));
+        put.addHeader("Content-Length", "128");
+
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, put);
+
+        verifyMocks();
+    }
+
+    /*
+     * "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 {
+        emptyMockCacheExpectsNoPuts();
+
+        request = new BasicHttpRequest("DELETE", "/", HTTP_1_1);
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+    }
+
+    /*
+     * "A TRACE request MUST NOT include an entity."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
+     */
+    @Test
+    public void testForwardedTRACERequestsDoNotIncludeAnEntity() throws Exception {
+        BasicHttpEntityEnclosingRequest trace = new BasicHttpEntityEnclosingRequest("TRACE", "/",
+                HTTP_1_1);
+        trace.setEntity(makeBody(entityLength));
+        trace.setHeader("Content-Length", Integer.toString(entityLength));
+
+        Capture<HttpRequest> reqCap = new Capture<HttpRequest>();
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.capture(reqCap),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, trace);
+        verifyMocks();
+
+        HttpRequest forwarded = reqCap.getValue();
+        if (forwarded instanceof HttpEntityEnclosingRequest) {
+            HttpEntityEnclosingRequest bodyReq = (HttpEntityEnclosingRequest) forwarded;
+            Assert.assertTrue(bodyReq.getEntity() == null
+                    || bodyReq.getEntity().getContentLength() == 0);
+        } else {
+            // request didn't enclose an entity
+        }
+    }
+
+    /*
+     * "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 {
+        emptyMockCacheExpectsNoPuts();
+
+        request = new BasicHttpRequest("TRACE", "/", HTTP_1_1);
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        impl.execute(host, request);
+
+        verifyMocks();
+    }
+
+    /*
+     * "The 204 response MUST NOT include a message-body, and thus is always
+     * terminated by the first empty line after the header fields."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
+     */
+    @Test
+    public void test204ResponsesDoNotContainMessageBodies() throws Exception {
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content");
+        originResponse.setEntity(makeBody(entityLength));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+    }
+
+    /*
+     * "10.2.6 205 Reset Content ... The response MUST NOT include an entity."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.6
+     */
+    @Test
+    public void test205ResponsesDoNotContainMessageBodies() throws Exception {
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_RESET_CONTENT,
+                "Reset Content");
+        originResponse.setEntity(makeBody(entityLength));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+    }
+
+    /*
+     * "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 {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "\"etag\"");
+        resp1.setHeader("Cache-Control", "max-age=3600");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+        replayMocks();
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+        verifyMocks();
+
+        if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+            if (result.getFirstHeader("Content-Range") == null) {
+                HeaderElement elt = result.getFirstHeader("Content-Type").getElements()[0];
+                Assert.assertTrue("multipart/byteranges".equalsIgnoreCase(elt.getName()));
+                Assert.assertNotNull(elt.getParameterByName("boundary"));
+                Assert.assertNotNull(elt.getParameterByName("boundary").getValue());
+                Assert.assertFalse("".equals(elt.getParameterByName("boundary").getValue().trim()));
+            }
+        }
+    }
+
+    @Test
+    public void test206ResponseGeneratedFromCacheMustHaveABodyThatMatchesContentLengthHeaderIfPresent()
+            throws Exception {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "\"etag\"");
+        resp1.setHeader("Cache-Control", "max-age=3600");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+        replayMocks();
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+        verifyMocks();
+
+        if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+            Header h = result.getFirstHeader("Content-Length");
+            if (h != null) {
+                int contentLength = Integer.parseInt(h.getValue());
+                int bytesRead = 0;
+                InputStream i = result.getEntity().getContent();
+                while ((i.read()) != -1) {
+                    bytesRead++;
+                }
+                i.close();
+                Assert.assertEquals(contentLength, bytesRead);
+            }
+        }
+    }
+
+    @Test
+    public void test206ResponseGeneratedFromCacheMustHaveDateHeader() throws Exception {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "\"etag\"");
+        resp1.setHeader("Cache-Control", "max-age=3600");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(resp1).times(1, 2);
+
+        replayMocks();
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+        verifyMocks();
+
+        if (HttpStatus.SC_PARTIAL_CONTENT == result.getStatusLine().getStatusCode()) {
+            Assert.assertNotNull(result.getFirstHeader("Date"));
+        }
+    }
+
+    @Test
+    public void test206ResponseReturnedToClientMustHaveDateHeader() throws Exception {
+        request.addHeader("Range", "bytes=0-50");
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+                "Partial Content");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        originResponse.setHeader("Server", "MockOrigin/1.0");
+        originResponse.setEntity(makeBody(500));
+        originResponse.setHeader("Content-Range", "bytes 0-499/1234");
+        originResponse.removeHeaders("Date");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+        Assert.assertTrue(result.getStatusLine().getStatusCode() != HttpStatus.SC_PARTIAL_CONTENT
+                || result.getFirstHeader("Date") != null);
+
+        verifyMocks();
+    }
+
+    @Test
+    public void test206ContainsETagIfA200ResponseWouldHaveIncludedIt() throws Exception {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+        originResponse.addHeader("ETag", "\"etag1\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.addHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+            Assert.assertNotNull(result.getFirstHeader("ETag"));
+        }
+    }
+
+    @Test
+    public void test206ContainsContentLocationIfA200ResponseWouldHaveIncludedIt() throws Exception {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+        originResponse.addHeader("Content-Location", "http://foo.example.com/other/url");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.addHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+            Assert.assertNotNull(result.getFirstHeader("Content-Location"));
+        }
+    }
+
+    @Test
+    public void test206ResponseIncludesVariantHeadersIfValueMightDiffer() throws Exception {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req1.addHeader("Accept-Encoding", "gzip");
+
+        Date now = new Date();
+        Date inOneHour = new Date(now.getTime() + 3600 * 1000L);
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+        originResponse.addHeader("Expires", DateUtils.formatDate(inOneHour));
+        originResponse.addHeader("Vary", "Accept-Encoding");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.addHeader("Cache-Control", "no-cache");
+        req2.addHeader("Accept-Encoding", "gzip");
+        Date nextSecond = new Date(now.getTime() + 1000L);
+        Date inTwoHoursPlusASec = new Date(now.getTime() + 2 * 3600 * 1000L + 1000L);
+
+        HttpResponse originResponse2 = make200Response();
+        originResponse2.setHeader("Date", DateUtils.formatDate(nextSecond));
+        originResponse2.setHeader("Cache-Control", "max-age=7200");
+        originResponse2.setHeader("Expires", DateUtils.formatDate(inTwoHoursPlusASec));
+        originResponse2.setHeader("Vary", "Accept-Encoding");
+
+        HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req3.addHeader("Range", "bytes=0-50");
+        req3.addHeader("Accept-Encoding", "gzip");
+
+        backendExpectsAnyRequest().andReturn(originResponse);
+        backendExpectsAnyRequest().andReturn(originResponse2).times(1, 2);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+        HttpResponse result = impl.execute(host, req3);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+            Assert.assertNotNull(result.getFirstHeader("Expires"));
+            Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
+            Assert.assertNotNull(result.getFirstHeader("Vary"));
+        }
+    }
+
+    /*
+     * "If the [206] response is the result of an If-Range request that 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.2.7
+     */
+    @Test
+    public void test206ResponseToConditionalRangeRequestDoesNotIncludeOtherEntityHeaders()
+            throws Exception {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        Date now = new Date();
+        Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+        originResponse = make200Response();
+        originResponse.addHeader("Allow", "GET,HEAD");
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+        originResponse.addHeader("Content-Language", "en");
+        originResponse.addHeader("Content-Encoding", "identity");
+        originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+        originResponse.addHeader("Content-Length", "128");
+        originResponse.addHeader("Content-Type", "application/octet-stream");
+        originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+        originResponse.addHeader("ETag", "W/\"weak-tag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.addHeader("If-Range", "W/\"weak-tag\"");
+        req2.addHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+            Assert.assertNull(result.getFirstHeader("Allow"));
+            Assert.assertNull(result.getFirstHeader("Content-Encoding"));
+            Assert.assertNull(result.getFirstHeader("Content-Language"));
+            Assert.assertNull(result.getFirstHeader("Content-MD5"));
+            Assert.assertNull(result.getFirstHeader("Last-Modified"));
+        }
+    }
+
+    /*
+     * "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 {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        Date now = new Date();
+        Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+        originResponse.addHeader("Allow", "GET,HEAD");
+        originResponse.addHeader("Cache-Control", "max-age=3600");
+        originResponse.addHeader("Content-Language", "en");
+        originResponse.addHeader("Content-Encoding", "identity");
+        originResponse.addHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+        originResponse.addHeader("Content-Length", "128");
+        originResponse.addHeader("Content-Type", "application/octet-stream");
+        originResponse.addHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+        originResponse.addHeader("ETag", "\"strong-tag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.addHeader("If-Range", "\"strong-tag\"");
+        req2.addHeader("Range", "bytes=0-50");
+
+        backendExpectsAnyRequest().andReturn(originResponse).times(1, 2);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT) {
+            Assert.assertEquals("GET,HEAD", result.getFirstHeader("Allow").getValue());
+            Assert.assertEquals("max-age=3600", result.getFirstHeader("Cache-Control").getValue());
+            Assert.assertEquals("en", result.getFirstHeader("Content-Language").getValue());
+            Assert.assertEquals("identity", result.getFirstHeader("Content-Encoding").getValue());
+            Assert.assertEquals("Q2hlY2sgSW50ZWdyaXR5IQ==", result.getFirstHeader("Content-MD5")
+                    .getValue());
+            Assert.assertEquals(originResponse.getFirstHeader("Last-Modified").getValue(), result
+                    .getFirstHeader("Last-Modified").getValue());
+        }
+    }
+
+    /*
+     * "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 {
+
+        Date now = new Date();
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("Cache-Control", "max-age=3600");
+        resp1.setHeader("ETag", "\"etag1\"");
+        byte[] bytes1 = new byte[128];
+        for (int i = 0; i < bytes1.length; i++) {
+            bytes1[i] = (byte) 1;
+        }
+        resp1.setEntity(new ByteArrayEntity(bytes1));
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Cache-Control", "no-cache");
+        req2.setHeader("Range", "bytes=0-50");
+
+        Date inOneSecond = new Date(now.getTime() + 1000L);
+        HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+                "Partial Content");
+        resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+        resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
+        resp2.setHeader("ETag", "\"etag2\"");
+        resp2.setHeader("Content-Range", "bytes 0-50/128");
+        byte[] bytes2 = new byte[51];
+        for (int i = 0; i < bytes2.length; i++) {
+            bytes2[i] = (byte) 2;
+        }
+        resp2.setEntity(new ByteArrayEntity(bytes2));
+
+        Date inTwoSeconds = new Date(now.getTime() + 2000L);
+        HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp3 = make200Response();
+        resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
+        resp3.setHeader("Cache-Control", "max-age=3600");
+        resp3.setHeader("ETag", "\"etag2\"");
+        byte[] bytes3 = new byte[128];
+        for (int i = 0; i < bytes3.length; i++) {
+            bytes3[i] = (byte) 2;
+        }
+        resp3.setEntity(new ByteArrayEntity(bytes3));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp2);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+        HttpResponse result = impl.execute(host, req3);
+
+        verifyMocks();
+
+        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();
+        Assert.assertFalse(found1 && found2); // mixture of content
+    }
+
+    @Test
+    public void test206ResponseIsNotCombinedWithPreviousContentIfLastModifiedDoesNotMatch()
+            throws Exception {
+
+        Date now = new Date();
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        Date oneHourAgo = new Date(now.getTime() - 3600L);
+        resp1.setHeader("Cache-Control", "max-age=3600");
+        resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+        byte[] bytes1 = new byte[128];
+        for (int i = 0; i < bytes1.length; i++) {
+            bytes1[i] = (byte) 1;
+        }
+        resp1.setEntity(new ByteArrayEntity(bytes1));
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Cache-Control", "no-cache");
+        req2.setHeader("Range", "bytes=0-50");
+
+        Date inOneSecond = new Date(now.getTime() + 1000L);
+        HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+                "Partial Content");
+        resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+        resp2.setHeader("Server", resp1.getFirstHeader("Server").getValue());
+        resp2.setHeader("Last-Modified", DateUtils.formatDate(now));
+        resp2.setHeader("Content-Range", "bytes 0-50/128");
+        byte[] bytes2 = new byte[51];
+        for (int i = 0; i < bytes2.length; i++) {
+            bytes2[i] = (byte) 2;
+        }
+        resp2.setEntity(new ByteArrayEntity(bytes2));
+
+        Date inTwoSeconds = new Date(now.getTime() + 2000L);
+        HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp3 = make200Response();
+        resp3.setHeader("Date", DateUtils.formatDate(inTwoSeconds));
+        resp3.setHeader("Cache-Control", "max-age=3600");
+        resp3.setHeader("ETag", "\"etag2\"");
+        byte[] bytes3 = new byte[128];
+        for (int i = 0; i < bytes3.length; i++) {
+            bytes3[i] = (byte) 2;
+        }
+        resp3.setEntity(new ByteArrayEntity(bytes3));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp2);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+        HttpResponse result = impl.execute(host, req3);
+
+        verifyMocks();
+
+        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();
+        Assert.assertFalse(found1 && found2); // mixture of content
+    }
+
+    /*
+     * "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()) {
+            emptyMockCacheExpectsNoPuts();
+
+            request = new BasicHttpRequest("GET", "/", HTTP_1_1);
+            request.addHeader("Range", "bytes=0-50");
+
+            originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_PARTIAL_CONTENT,
+                    "Partial Content");
+            originResponse.setHeader("Content-Range", "bytes 0-50/128");
+            originResponse.setHeader("Cache-Control", "max-age=3600");
+            byte[] bytes = new byte[51];
+            (new Random()).nextBytes(bytes);
+            originResponse.setEntity(new ByteArrayEntity(bytes));
+
+            EasyMock.expect(
+                    mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
+                            .isA(HttpRequest.class), (HttpContext) EasyMock.isNull())).andReturn(
+                    originResponse);
+
+            replayMocks();
+            impl.execute(host, request);
+            verifyMocks();
+        }
+    }
+
+    /*
+     * "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 {
+        emptyMockCacheExpectsNoPuts();
+
+        request = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_SEE_OTHER, "See Other");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        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(makeBody(entityLength));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+    }
+
+    /*
+     * "The 304 response MUST NOT contain a message-body, and thus is always
+     * terminated by the first empty line after the header fields."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+     */
+    @Test
+    public void test304ResponseDoesNotContainABody() throws Exception {
+        request.setHeader("If-None-Match", "\"etag\"");
+
+        originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        originResponse.setHeader("Server", "MockServer/1.0");
+        originResponse.setHeader("Content-Length", "128");
+        originResponse.setEntity(makeBody(entityLength));
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+
+        Assert.assertTrue(result.getEntity() == null || result.getEntity().getContentLength() == 0);
+    }
+
+    /*
+     * "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 BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+        originResponse.setHeader("Date", DateUtils.formatDate(new Date()));
+        originResponse.setHeader("Server", "MockServer/1.0");
+        originResponse.setHeader("ETag", "\"etag\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Date"));
+    }
+
+    @Test
+    public void test304ResponseGeneratedFromCacheIncludesDateHeader() throws Exception {
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+        originResponse.setHeader("ETag", "\"etag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("If-None-Match", "\"etag\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+            Assert.assertNotNull(result.getFirstHeader("Date"));
+        }
+    }
+
+    /*
+     * "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 {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+        originResponse.setHeader("ETag", "\"etag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("If-None-Match", "\"etag\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+            Assert.assertNotNull(result.getFirstHeader("ETag"));
+        }
+    }
+
+    @Test
+    public void test304ResponseGeneratedFromCacheIncludesContentLocationIfOriginResponseDid()
+            throws Exception {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        originResponse.setHeader("Cache-Control", "max-age=3600");
+        originResponse.setHeader("Content-Location", "http://foo.example.com/other");
+        originResponse.setHeader("ETag", "\"etag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("If-None-Match", "\"etag\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse).times(1, 2);
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+            Assert.assertNotNull(result.getFirstHeader("Content-Location"));
+        }
+    }
+
+    /*
+     * "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 {
+
+        Date now = new Date();
+        Date inTwoHours = new Date(now.getTime() + 2 * 3600 * 1000L);
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req1.setHeader("Accept-Encoding", "gzip");
+
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "\"v1\"");
+        resp1.setHeader("Cache-Control", "max-age=7200");
+        resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours));
+        resp1.setHeader("Vary", "Accept-Encoding");
+        resp1.setEntity(makeBody(entityLength));
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req1.setHeader("Accept-Encoding", "gzip");
+        req1.setHeader("Cache-Control", "no-cache");
+
+        HttpResponse resp2 = make200Response();
+        resp2.setHeader("ETag", "\"v2\"");
+        resp2.setHeader("Cache-Control", "max-age=3600");
+        resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours));
+        resp2.setHeader("Vary", "Accept-Encoding");
+        resp2.setEntity(makeBody(entityLength));
+
+        HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req3.setHeader("Accept-Encoding", "gzip");
+        req3.setHeader("If-None-Match", "\"v2\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp2).times(1, 2);
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+        HttpResponse result = impl.execute(host, req3);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+            Assert.assertNotNull(result.getFirstHeader("Expires"));
+            Assert.assertNotNull(result.getFirstHeader("Cache-Control"));
+            Assert.assertNotNull(result.getFirstHeader("Vary"));
+        }
+    }
+
+    /*
+     * "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 {
+
+        Date now = new Date();
+        Date oneHourAgo = new Date(now.getTime() - 3600 * 1000L);
+
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "W/\"v1\"");
+        resp1.setHeader("Allow", "GET,HEAD");
+        resp1.setHeader("Content-Encoding", "identity");
+        resp1.setHeader("Content-Language", "en");
+        resp1.setHeader("Content-Length", "128");
+        resp1.setHeader("Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
+        resp1.setHeader("Content-Type", "application/octet-stream");
+        resp1.setHeader("Last-Modified", DateUtils.formatDate(oneHourAgo));
+        resp1.setHeader("Cache-Control", "max-age=7200");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("If-None-Match", "W/\"v1\"");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1).times(1, 2);
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        if (result.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
+            Assert.assertNull(result.getFirstHeader("Allow"));
+            Assert.assertNull(result.getFirstHeader("Content-Encoding"));
+            Assert.assertNull(result.getFirstHeader("Content-Length"));
+            Assert.assertNull(result.getFirstHeader("Content-MD5"));
+            Assert.assertNull(result.getFirstHeader("Content-Type"));
+            Assert.assertNull(result.getFirstHeader("Last-Modified"));
+        }
+    }
+
+    /*
+     * "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 {
+
+        Date now = new Date();
+
+        // load cache with cacheable entry
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("ETag", "\"etag1\"");
+        resp1.setHeader("Cache-Control", "max-age=3600");
+
+        // force a revalidation
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
+
+        // updated ETag provided to a conditional revalidation
+        HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
+                "Not Modified");
+        resp2.setHeader("Date", DateUtils.formatDate(now));
+        resp2.setHeader("Server", "MockServer/1.0");
+        resp2.setHeader("ETag", "\"etag2\"");
+
+        // conditional validation uses If-None-Match
+        HttpRequest conditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        conditionalValidation.setHeader("If-None-Match", "\"etag1\"");
+
+        // unconditional validation doesn't use If-None-Match
+        HttpRequest unconditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        // new response to unconditional validation provides new body
+        HttpResponse resp3 = make200Response();
+        resp1.setHeader("ETag", "\"etag2\"");
+        resp1.setHeader("Cache-Control", "max-age=3600");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1);
+        // this next one will happen once if the cache tries to
+        // conditionally validate, zero if it goes full revalidation
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(conditionalValidation),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp2).times(0, 1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), eqRequest(unconditionalValidation),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp3);
+        replayMocks();
+
+        impl.execute(host, req1);
+        impl.execute(host, req2);
+
+        verifyMocks();
+    }
+
+    /*
+     * "If a cache uses a received 304 response to update a cache entry, the
+     * cache MUST update 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 {
+
+        Date now = new Date();
+        Date inOneSecond = new Date(now.getTime() + 1000L);
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("Cache-Control", "max-age=3600");
+        resp1.setHeader("ETag", "\"etag\"");
+
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        req2.setHeader("Cache-Control", "max-age=0,max-stale=0");
+
+        HttpRequest conditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+        conditionalValidation.setHeader("If-None-Match", "\"etag\"");
+
+        HttpRequest unconditionalValidation = new BasicHttpRequest("GET", "/", HTTP_1_1);
+
+        // to be used if the cache generates a conditional validation
+        HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NOT_MODIFIED,
+                "Not Modified");
+        resp2.setHeader("Date", DateUtils.formatDate(inOneSecond));
+        resp2.setHeader("Server", "MockUtils/1.0");
+        resp2.setHeader("ETag", "\"etag\"");
+        resp2.setHeader("X-Extra", "junk");
+
+        // to be used if the cache generates an unconditional validation
+        HttpResponse resp3 = make200Response();
+        resp3.setHeader("Date", DateUtils.formatDate(inOneSecond));
+        resp3.setHeader("ETag", "\"etag\"");
+
+        Capture<HttpRequest> cap1 = new Capture<HttpRequest>();
+        Capture<HttpRequest> cap2 = new Capture<HttpRequest>();
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.and(
+                        eqRequest(conditionalValidation), EasyMock.capture(cap1)),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp2).times(0, 1);
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.eq(host), EasyMock.and(
+                        eqRequest(unconditionalValidation), EasyMock.capture(cap2)),
+                        (HttpContext) EasyMock.isNull())).andReturn(resp3).times(0, 1);
+
+        replayMocks();
+
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+
+        verifyMocks();
+
+        Assert.assertTrue((cap1.hasCaptured() && !cap2.hasCaptured())
+                || (!cap1.hasCaptured() && cap2.hasCaptured()));
+
+        if (cap1.hasCaptured()) {
+            Assert.assertEquals(DateUtils.formatDate(inOneSecond), result.getFirstHeader("Date")
+                    .getValue());
+            Assert.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 BasicHttpResponse(HTTP_1_1, 401, "Unauthorized");
+        originResponse.setHeader("WWW-Authenticate", "x-scheme x-param");
+
+        EasyMock.expect(
+                mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class),
+                        (HttpContext) EasyMock.isNull())).andReturn(originResponse);
+        replayMocks();
+
+        HttpResponse result = impl.execute(host, request);
+        if (result.getStatusLine().getStatusCode() == 401) {
+            Assert.assertNotNull(result.getFirstHeader("WWW-Authenticate"));
+        }
+
+        verifyMocks();
+    }
+
+    /*
+     * "10.4.6 405 Method Not Allowed ... The response MUST include an Allow
+     * header containing a list of valid methods for the requested resource.
+     *

[... 996 lines stripped ...]


Mime
View raw message