/*
* ====================================================================
* 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.http.impl.client.cache;
import static org.easymock.classextension.EasyMock.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
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.HttpVersion;
import static org.apache.http.impl.cookie.DateUtils.formatDate;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
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.EasyMock;
import org.junit.Before;
import org.junit.Test;
/*
* 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.
*/
public class TestProtocolRecommendations extends AbstractProtocolTest {
private Date tenSecondsFromNow;
private Date now;
private Date tenSecondsAgo;
private Date twoMinutesAgo;
@Override
@Before
public void setUp() {
super.setUp();
now = new Date();
tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000L);
tenSecondsFromNow = new Date(now.getTime() + 10 * 1000L);
}
/* "identity: The default (identity) encoding; the use of no
* transformation whatsoever. This content-coding is used only in the
* Accept-Encoding header, and SHOULD NOT be used in the
* Content-Encoding header."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5
*/
@Test
public void testIdentityCodingIsNotUsedInContentEncodingHeader()
throws Exception {
originResponse.setHeader("Content-Encoding", "identity");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
HttpResponse result = impl.execute(host, request);
verifyMocks();
boolean foundIdentity = false;
for(Header h : result.getHeaders("Content-Encoding")) {
for(HeaderElement elt : h.getElements()) {
if ("identity".equalsIgnoreCase(elt.getName())) {
foundIdentity = true;
}
}
}
assertFalse(foundIdentity);
}
/*
* "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(
String headerName, String headerValue, String validatorHeader,
String validator, String conditionalHeader) throws Exception,
IOException {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader(validatorHeader, validator);
resp1.setHeader(headerName, headerValue);
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader(conditionalHeader, validator);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
if (HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode()) {
assertNull(result.getFirstHeader(headerName));
}
}
private void cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
String headerName, String headerValue) throws Exception,
IOException {
cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
headerValue, "ETag", "\"etag\"", "If-None-Match");
}
private void cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
String headerName, String headerValue) throws Exception,
IOException {
cacheGenerated304ForValidatorShouldNotContainEntityHeader(headerName,
headerValue, "Last-Modified", formatDate(twoMinutesAgo),
"If-Modified-Since");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainAllow()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Allow", "GET,HEAD");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainAllow()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Allow", "GET,HEAD");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentEncoding()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Content-Encoding", "gzip");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentEncoding()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Content-Encoding", "gzip");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentLanguage()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Content-Language", "en");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLanguage()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Content-Language", "en");
}
@Test
public void cacheGenerated304ForStrongValidatorShouldNotContainContentLength()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Content-Length", "128");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentLength()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Content-Length", "128");
}
@Test
public void cacheGenerated304ForStrongValidatorShouldNotContainContentMD5()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentMD5()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
private void cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
String validatorHeader, String validator, String conditionalHeader)
throws Exception, IOException, ClientProtocolException {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
req1.setHeader("Range","bytes=0-127");
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, 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");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("If-Range", validator);
req2.setHeader("Range","bytes=0-127");
req2.setHeader(conditionalHeader, validator);
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Date", formatDate(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
Capture cap = new Capture();
expect(mockBackend.execute(same(host), capture(cap), (HttpContext)isNull()))
.andReturn(resp2).times(0,1);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
if (!cap.hasCaptured()
&& HttpStatus.SC_NOT_MODIFIED == result.getStatusLine().getStatusCode()) {
// cache generated a 304
assertNull(result.getFirstHeader("Content-Range"));
}
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentRange()
throws Exception {
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
"ETag", "\"etag\"", "If-None-Match");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentRange()
throws Exception {
cacheGenerated304ForStrongValidatorShouldNotContainContentRange(
"Last-Modified", formatDate(twoMinutesAgo), "If-Modified-Since");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainContentType()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Content-Type", "text/html");
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainContentType()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Content-Type", "text/html");
}
@Test
public void cacheGenerated304ForStrongEtagValidatorShouldNotContainLastModified()
throws Exception {
cacheGenerated304ForStrongETagValidatorShouldNotContainEntityHeader(
"Last-Modified", formatDate(tenSecondsAgo));
}
@Test
public void cacheGenerated304ForStrongDateValidatorShouldNotContainLastModified()
throws Exception {
cacheGenerated304ForStrongDateValidatorShouldNotContainEntityHeader(
"Last-Modified", formatDate(twoMinutesAgo));
}
private void shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
String entityHeader, String entityHeaderValue) throws Exception,
IOException {
HttpRequest req = HttpTestUtils.makeDefaultRequest();
req.setHeader("If-None-Match", "\"etag\"");
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp.setHeader("Date", formatDate(now));
resp.setHeader("Etag", "\"etag\"");
resp.setHeader(entityHeader, entityHeaderValue);
backendExpectsAnyRequest().andReturn(resp);
replayMocks();
HttpResponse result = impl.execute(host, req);
verifyMocks();
assertNull(result.getFirstHeader(entityHeader));
}
@Test
public void shouldStripAllowFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Allow", "GET,HEAD");
}
@Test
public void shouldStripContentEncodingFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Content-Encoding", "gzip");
}
@Test
public void shouldStripContentLanguageFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Content-Language", "en");
}
@Test
public void shouldStripContentLengthFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Content-Length", "128");
}
@Test
public void shouldStripContentMD5FromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Content-MD5", "Q2hlY2sgSW50ZWdyaXR5IQ==");
}
@Test
public void shouldStripContentTypeFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Content-Type", "text/html;charset=utf-8");
}
@Test
public void shouldStripContentRangeFromOrigin304ResponseToStringValidation()
throws Exception {
HttpRequest req = HttpTestUtils.makeDefaultRequest();
req.setHeader("If-Range","\"etag\"");
req.setHeader("Range","bytes=0-127");
HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp.setHeader("Date", formatDate(now));
resp.setHeader("ETag", "\"etag\"");
resp.setHeader("Content-Range", "bytes 0-127/256");
backendExpectsAnyRequest().andReturn(resp);
replayMocks();
HttpResponse result = impl.execute(host, req);
verifyMocks();
assertNull(result.getFirstHeader("Content-Range"));
}
@Test
public void shouldStripLastModifiedFromOrigin304ResponseToStrongValidation()
throws Exception {
shouldStripEntityHeaderFromOrigin304ResponseToStrongValidation(
"Last-Modified", formatDate(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 HttpRequest requestToPopulateStaleCacheEntry() throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","public,max-age=5");
resp1.setHeader("Etag","\"etag\"");
backendExpectsAnyRequest().andReturn(resp1);
return req1;
}
private void testDoesNotReturnStaleResponseOnError(HttpRequest req2)
throws Exception, IOException {
HttpRequest req1 = requestToPopulateStaleCacheEntry();
backendExpectsAnyRequest().andThrow(new IOException());
replayMocks();
impl.execute(host, req1);
HttpResponse result = null;
try {
result = impl.execute(host, req2);
} catch (IOException acceptable) {
}
verifyMocks();
if (result != null) {
assertFalse(result.getStatusLine().getStatusCode() == HttpStatus.SC_OK);
}
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithCacheControl()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Cache-Control","no-cache");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFirstHandOneWithPragma()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Pragma","no-cache");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxAge()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Cache-Control","max-age=0");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlySpecifiesLargerMaxAge()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Cache-Control","max-age=20");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMinFresh()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Cache-Control","min-fresh=2");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testDoesNotReturnStaleResponseIfClientExplicitlyRequestsFreshWithMaxStale()
throws Exception {
HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req.setHeader("Cache-Control","max-stale=2");
testDoesNotReturnStaleResponseOnError(req);
}
@Test
public void testMayReturnStaleResponseIfClientExplicitlySpecifiesAcceptableMaxStale()
throws Exception {
HttpRequest req1 = requestToPopulateStaleCacheEntry();
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("Cache-Control","max-stale=20");
backendExpectsAnyRequest().andThrow(new IOException()).times(0,1);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertEquals(HttpStatus.SC_OK, result.getStatusLine().getStatusCode());
assertNotNull(result.getFirstHeader("Warning"));
}
/*
* "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.
*
* If the cache can not communicate with the origin server, then a
* correct cache SHOULD respond as above if the response can be
* correctly served from the cache..."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
*/
@Test
public void testReturnsCachedResponsesAppropriatelyWhenNoOriginCommunication()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
resp1.setHeader("Cache-Control", "public, max-age=5");
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Date", formatDate(tenSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
backendExpectsAnyRequest().andThrow(new IOException()).anyTimes();
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertEquals(HttpStatus.SC_OK, result.getStatusLine().getStatusCode());
boolean warning111Found = false;
for(Header h : result.getHeaders("Warning")) {
for(WarningValue wv : WarningValue.getWarningValues(h)) {
if (wv.getWarnCode() == 111) {
warning111Found = true;
break;
}
}
}
assertTrue(warning111Found);
}
/*
* "If a cache receives a response (either an entire response, or a
* 304 (Not Modified) response) that it would normally forward to the
* requesting client, and the received response is no longer fresh,
* the cache SHOULD forward it to the requesting client without adding
* a new Warning (but without removing any existing Warning headers).
* A cache SHOULD NOT attempt to revalidate a response simply because
* that response became stale in transit; this might lead to an
* infinite loop."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
*/
@Test
public void testDoesNotAddNewWarningHeaderIfResponseArrivesStale()
throws Exception {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
originResponse.setHeader("Date", formatDate(tenSecondsAgo));
originResponse.setHeader("Cache-Control","public, max-age=5");
originResponse.setHeader("ETag","\"etag\"");
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
HttpResponse result = impl.execute(host, request);
verifyMocks();
assertNull(result.getFirstHeader("Warning"));
}
@Test
public void testForwardsExistingWarningHeadersOnResponseThatArrivesStale()
throws Exception {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
originResponse.setHeader("Date", formatDate(tenSecondsAgo));
originResponse.setHeader("Cache-Control","public, max-age=5");
originResponse.setHeader("ETag","\"etag\"");
originResponse.addHeader("Age","10");
final String warning = "110 fred \"Response is stale\"";
originResponse.addHeader("Warning",warning);
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
HttpResponse result = impl.execute(host, request);
verifyMocks();
assertEquals(warning, result.getFirstHeader("Warning").getValue());
}
/*
* "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);
backendExpectsAnyRequest().andReturn(originResponse);
replayMocks();
HttpResponse result = impl.execute(host, request);
verifyMocks();
assertEquals(headerValue,
result.getFirstHeader(headerName).getValue());
}
private void testDoesNotModifyHeaderOnRequests(final String headerName)
throws Exception {
final String headerValue = HttpTestUtils.getCanonicalHeaderValue(request, headerName);
Capture cap = new Capture();
EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
.andReturn(originResponse);
replayMocks();
impl.execute(host, request);
verifyMocks();
assertEquals(headerValue,
HttpTestUtils.getCanonicalHeaderValue(cap.getValue(),
headerName));
}
@Test
public void testDoesNotModifyAcceptRangesOnResponses()
throws Exception {
final String headerName = "Accept-Ranges";
originResponse.setHeader(headerName,"bytes");
testDoesNotModifyHeaderOnResponses(headerName);
}
@Test
public void testDoesNotModifyAuthorizationOnRequests()
throws Exception {
request.setHeader("Authorization", "Basic dXNlcjpwYXNzd2Q=");
testDoesNotModifyHeaderOnRequests("Authorization");
}
@Test
public void testDoesNotModifyContentLengthOnRequests()
throws Exception {
HttpEntityEnclosingRequest post =
new BasicHttpEntityEnclosingRequest("POST", "/", HttpVersion.HTTP_1_1);
post.setEntity(HttpTestUtils.makeBody(128));
post.setHeader("Content-Length","128");
request = post;
testDoesNotModifyHeaderOnRequests("Content-Length");
}
@Test
public void testDoesNotModifyContentLengthOnResponses()
throws Exception {
originResponse.setEntity(HttpTestUtils.makeBody(128));
originResponse.setHeader("Content-Length","128");
testDoesNotModifyHeaderOnResponses("Content-Length");
}
@Test
public void testDoesNotModifyContentMD5OnRequests()
throws Exception {
HttpEntityEnclosingRequest post =
new BasicHttpEntityEnclosingRequest("POST", "/", HttpVersion.HTTP_1_1);
post.setEntity(HttpTestUtils.makeBody(128));
post.setHeader("Content-Length","128");
post.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
request = post;
testDoesNotModifyHeaderOnRequests("Content-MD5");
}
@Test
public void testDoesNotModifyContentMD5OnResponses()
throws Exception {
originResponse.setEntity(HttpTestUtils.makeBody(128));
originResponse.setHeader("Content-MD5","Q2hlY2sgSW50ZWdyaXR5IQ==");
testDoesNotModifyHeaderOnResponses("Content-MD5");
}
@Test
public void testDoesNotModifyContentRangeOnRequests()
throws Exception {
HttpEntityEnclosingRequest put =
new BasicHttpEntityEnclosingRequest("PUT", "/", HttpVersion.HTTP_1_1);
put.setEntity(HttpTestUtils.makeBody(128));
put.setHeader("Content-Length","128");
put.setHeader("Content-Range","bytes 0-127/256");
request = put;
testDoesNotModifyHeaderOnRequests("Content-Range");
}
@Test
public void testDoesNotModifyContentRangeOnResponses()
throws Exception {
request.setHeader("Range","bytes=0-128");
originResponse.setStatusCode(HttpStatus.SC_PARTIAL_CONTENT);
originResponse.setReasonPhrase("Partial Content");
originResponse.setEntity(HttpTestUtils.makeBody(128));
originResponse.setHeader("Content-Range","bytes 0-127/256");
testDoesNotModifyHeaderOnResponses("Content-Range");
}
@Test
public void testDoesNotModifyContentTypeOnRequests()
throws Exception {
HttpEntityEnclosingRequest post =
new BasicHttpEntityEnclosingRequest("POST", "/", HttpVersion.HTTP_1_1);
post.setEntity(HttpTestUtils.makeBody(128));
post.setHeader("Content-Length","128");
post.setHeader("Content-Type","application/octet-stream");
request = post;
testDoesNotModifyHeaderOnRequests("Content-Type");
}
@Test
public void testDoesNotModifyContentTypeOnResponses()
throws Exception {
originResponse.setHeader("Content-Type","application/octet-stream");
testDoesNotModifyHeaderOnResponses("Content-Type");
}
@Test
public void testDoesNotModifyDateOnRequests()
throws Exception {
request.setHeader("Date", formatDate(new Date()));
testDoesNotModifyHeaderOnRequests("Date");
}
@Test
public void testDoesNotModifyDateOnResponses()
throws Exception {
originResponse.setHeader("Date", formatDate(new Date()));
testDoesNotModifyHeaderOnResponses("Date");
}
@Test
public void testDoesNotModifyETagOnResponses()
throws Exception {
originResponse.setHeader("ETag", "\"random-etag\"");
testDoesNotModifyHeaderOnResponses("ETag");
}
@Test
public void testDoesNotModifyExpiresOnResponses()
throws Exception {
originResponse.setHeader("Expires", formatDate(new Date()));
testDoesNotModifyHeaderOnResponses("Expires");
}
@Test
public void testDoesNotModifyFromOnRequests()
throws Exception {
request.setHeader("From", "foo@example.com");
testDoesNotModifyHeaderOnRequests("From");
}
@Test
public void testDoesNotModifyIfMatchOnRequests()
throws Exception {
request = new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1);
request.setHeader("If-Match", "\"etag\"");
testDoesNotModifyHeaderOnRequests("If-Match");
}
@Test
public void testDoesNotModifyIfModifiedSinceOnRequests()
throws Exception {
request.setHeader("If-Modified-Since", formatDate(new Date()));
testDoesNotModifyHeaderOnRequests("If-Modified-Since");
}
@Test
public void testDoesNotModifyIfNoneMatchOnRequests()
throws Exception {
request.setHeader("If-None-Match", "\"etag\"");
testDoesNotModifyHeaderOnRequests("If-None-Match");
}
@Test
public void testDoesNotModifyIfRangeOnRequests()
throws Exception {
request.setHeader("Range","bytes=0-128");
request.setHeader("If-Range", "\"etag\"");
testDoesNotModifyHeaderOnRequests("If-Range");
}
@Test
public void testDoesNotModifyIfUnmodifiedSinceOnRequests()
throws Exception {
request = new BasicHttpRequest("DELETE", "/", HttpVersion.HTTP_1_1);
request.setHeader("If-Unmodified-Since", formatDate(new Date()));
testDoesNotModifyHeaderOnRequests("If-Unmodified-Since");
}
@Test
public void testDoesNotModifyLastModifiedOnResponses()
throws Exception {
originResponse.setHeader("Last-Modified", formatDate(new Date()));
testDoesNotModifyHeaderOnResponses("Last-Modified");
}
@Test
public void testDoesNotModifyLocationOnResponses()
throws Exception {
originResponse.setStatusCode(HttpStatus.SC_TEMPORARY_REDIRECT);
originResponse.setReasonPhrase("Temporary Redirect");
originResponse.setHeader("Location", "http://foo.example.com/bar");
testDoesNotModifyHeaderOnResponses("Location");
}
@Test
public void testDoesNotModifyRangeOnRequests()
throws Exception {
request.setHeader("Range", "bytes=0-128");
testDoesNotModifyHeaderOnRequests("Range");
}
@Test
public void testDoesNotModifyRefererOnRequests()
throws Exception {
request.setHeader("Referer", "http://foo.example.com/bar");
testDoesNotModifyHeaderOnRequests("Referer");
}
@Test
public void testDoesNotModifyRetryAfterOnResponses()
throws Exception {
originResponse.setStatusCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
originResponse.setReasonPhrase("Service Unavailable");
originResponse.setHeader("Retry-After", "120");
testDoesNotModifyHeaderOnResponses("Retry-After");
}
@Test
public void testDoesNotModifyServerOnResponses()
throws Exception {
originResponse.setHeader("Server", "SomeServer/1.0");
testDoesNotModifyHeaderOnResponses("Server");
}
@Test
public void testDoesNotModifyUserAgentOnRequests()
throws Exception {
request.setHeader("User-Agent", "MyClient/1.0");
testDoesNotModifyHeaderOnRequests("User-Agent");
}
@Test
public void testDoesNotModifyVaryOnResponses()
throws Exception {
request.setHeader("Accept-Encoding","identity");
originResponse.setHeader("Vary", "Accept-Encoding");
testDoesNotModifyHeaderOnResponses("Vary");
}
@Test
public void testDoesNotModifyExtensionHeaderOnRequests()
throws Exception {
request.setHeader("X-Extension","x-value");
testDoesNotModifyHeaderOnRequests("X-Extension");
}
@Test
public void testDoesNotModifyExtensionHeaderOnResponses()
throws Exception {
originResponse.setHeader("X-Extension", "x-value");
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 {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
final String lmDate = formatDate(twentySecondsAgo);
HttpRequest req1 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Last-Modified", lmDate);
resp1.setHeader("Cache-Control","max-age=5");
backendExpectsAnyRequest().andReturn(resp1);
Capture cap = new Capture();
HttpRequest req2 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp2 = HttpTestUtils.make200Response();
EasyMock.expect(mockBackend.execute(EasyMock.same(host),
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
.andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
HttpRequest captured = cap.getValue();
Header ifModifiedSince =
captured.getFirstHeader("If-Modified-Since");
assertEquals(lmDate, ifModifiedSince.getValue());
}
/*
* "[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 {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
final String lmDate = formatDate(twentySecondsAgo);
final String etag = "\"etag\"";
HttpRequest req1 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Last-Modified", lmDate);
resp1.setHeader("Cache-Control","max-age=5");
resp1.setHeader("ETag", etag);
backendExpectsAnyRequest().andReturn(resp1);
Capture cap = new Capture();
HttpRequest req2 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp2 = HttpTestUtils.make200Response();
EasyMock.expect(mockBackend.execute(EasyMock.same(host),
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
.andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
HttpRequest captured = cap.getValue();
Header ifModifiedSince =
captured.getFirstHeader("If-Modified-Since");
assertEquals(lmDate, ifModifiedSince.getValue());
Header ifNoneMatch =
captured.getFirstHeader("If-None-Match");
assertEquals(etag, ifNoneMatch.getValue());
}
/*
* "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 {
Date now = new Date();
Date oneSecondAgo = new Date(now.getTime() - 1 * 1000L);
Date oneSecondFromNow = new Date(now.getTime() + 1 * 1000L);
Date twoSecondsFromNow = new Date(now.getTime() + 2 * 1000L);
HttpRequest req1 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Date", formatDate(now));
resp1.setHeader("Expires",formatDate(oneSecondAgo));
resp1.setHeader("Cache-Control", "public");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpRequest revalidate =
new BasicHttpRequest("GET", "/",HttpVersion.HTTP_1_1);
revalidate.setHeader("If-None-Match","\"etag\"");
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Date", formatDate(twoSecondsFromNow));
resp2.setHeader("Expires", formatDate(oneSecondFromNow));
resp2.setHeader("ETag","\"etag\"");
expect(mockBackend.execute(isA(HttpHost.class),
eqRequest(revalidate), (HttpContext)isNull()))
.andReturn(resp2);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertEquals(HttpStatus.SC_OK,
result.getStatusLine().getStatusCode());
}
/* "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 {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
Date elevenSecondsAgo = new Date(now.getTime() - 11 * 1000L);
HttpRequest req1 =
new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("ETag","\"etag\"");
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","max-age=5");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1,
HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("ETag","\"etag\"");
resp2.setHeader("Date", formatDate(elevenSecondsAgo));
backendExpectsAnyRequest().andReturn(resp2);
Capture cap = new Capture();
HttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("ETag","\"etag2\"");
resp3.setHeader("Date", formatDate(now));
resp3.setHeader("Cache-Control","max-age=5");
expect(mockBackend.execute(isA(HttpHost.class), capture(cap),
(HttpContext)isNull()))
.andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
HttpRequest captured = cap.getValue();
boolean hasMaxAge0 = false;
boolean hasNoCache = false;
for(Header h : captured.getHeaders("Cache-Control")) {
for(HeaderElement elt : h.getElements()) {
if ("max-age".equals(elt.getName())) {
try {
int maxage = Integer.parseInt(elt.getValue());
if (maxage == 0) {
hasMaxAge0 = true;
}
} catch (NumberFormatException nfe) {
// nop
}
} else if ("no-cache".equals(elt.getName())) {
hasNoCache = true;
}
}
}
assertTrue(hasMaxAge0 || hasNoCache);
assertNull(captured.getFirstHeader("If-None-Match"));
assertNull(captured.getFirstHeader("If-Modified-Since"));
assertNull(captured.getFirstHeader("If-Range"));
assertNull(captured.getFirstHeader("If-Match"));
assertNull(captured.getFirstHeader("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
*/
@Test
public void testSendsAllVariantEtagsInConditionalRequest()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1);
req1.setHeader("User-Agent","agent1");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary","User-Agent");
resp1.setHeader("Etag","\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1);
req2.setHeader("User-Agent","agent2");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Vary","User-Agent");
resp2.setHeader("Etag","\"etag2\"");
backendExpectsAnyRequest().andReturn(resp2);
Capture cap = new Capture();
HttpRequest req3 = new BasicHttpRequest("GET","/",HttpVersion.HTTP_1_1);
req3.setHeader("User-Agent","agent3");
HttpResponse resp3 = HttpTestUtils.make200Response();
EasyMock.expect(mockBackend.execute(EasyMock.eq(host),
EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
.andReturn(resp3);
replayMocks();
impl.execute(host,req1);
impl.execute(host,req2);
impl.execute(host,req3);
verifyMocks();
HttpRequest captured = cap.getValue();
boolean foundEtag1 = false;
boolean foundEtag2 = false;
for(Header h : captured.getHeaders("If-None-Match")) {
for(String etag : h.getValue().split(",")) {
if ("\"etag1\"".equals(etag.trim())) {
foundEtag1 = true;
}
if ("\"etag2\"".equals(etag.trim())) {
foundEtag2 = true;
}
}
}
assertTrue(foundEtag1 && foundEtag2);
}
/* "If the entity-tag of the new response matches that of an existing
* entry, the new response SHOULD be used to update 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 {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req1.setHeader("User-Agent", "agent1");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("User-Agent", "agent2");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Date", formatDate(tenSecondsAgo));
resp2.setHeader("Vary", "User-Agent");
resp2.setHeader("Cache-Control", "max-age=3600");
resp2.setHeader("ETag", "\"etag2\"");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req3.setHeader("User-Agent", "agent3");
HttpResponse resp3 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp3.setHeader("Date", formatDate(now));
resp3.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp3);
HttpRequest req4 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req4.setHeader("User-Agent", "agent1");
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
HttpResponse result1 = impl.execute(host, req3);
HttpResponse result2 = impl.execute(host, req4);
verifyMocks();
assertEquals(HttpStatus.SC_OK, result1.getStatusLine().getStatusCode());
assertEquals("\"etag1\"", result1.getFirstHeader("ETag").getValue());
assertEquals(formatDate(now), result1.getFirstHeader("Date").getValue());
assertEquals(formatDate(now), result2.getFirstHeader("Date").getValue());
}
@Test
public void testResponseToExistingVariantsIsCachedForFutureResponses()
throws Exception {
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req1.setHeader("User-Agent", "agent1");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("User-Agent", "agent2");
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
resp2.setHeader("Date", formatDate(now));
resp2.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req3.setHeader("User-Agent", "agent2");
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
/* "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 {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
req1.setHeader("User-Agent", "agent1");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("User-Agent", "agent2");
req2.setHeader("Range", "bytes=0-49");
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1,
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", formatDate(new Date()));
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = HttpTestUtils.makeDefaultRequest();
req3.setHeader("User-Agent", "agent3");
HttpResponse resp3 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("Vary", "User-Agent");
resp1.setHeader("ETag", "\"etag3\"");
Capture cap = new Capture();
expect(mockBackend.execute(isA(HttpHost.class), capture(cap),
(HttpContext)isNull())).andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
HttpRequest captured = cap.getValue();
for(Header h : captured.getHeaders("If-None-Match")) {
for(HeaderElement elt : h.getElements()) {
assertFalse("\"etag2\"".equals(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 {
HttpRequest req1 = new HttpGet("http://foo.example.com/");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("ETag", "\"old-etag\"");
resp1.setHeader("Date", formatDate(tenSecondsAgo));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new HttpGet("http://foo.example.com/bar");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"new-etag\"");
resp2.setHeader("Date", formatDate(now));
resp2.setHeader("Content-Location", "http://foo.example.com/");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new HttpGet("http://foo.example.com");
HttpResponse resp3 = HttpTestUtils.make200Response();
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
verifyMocks();
}
/*
* "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_0OriginIsNotCached()
throws Exception {
HttpRequest req1 = new HttpGet("http://foo.example.com/bar?baz=quux");
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_OK, "OK");
resp1.setEntity(HttpTestUtils.makeBody(200));
resp1.setHeader("Content-Length","200");
resp1.setHeader("Date", formatDate(now));
resp1.setHeader("Expires", formatDate(tenSecondsFromNow));
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_OK, "OK");
resp2.setEntity(HttpTestUtils.makeBody(200));
resp2.setHeader("Content-Length","200");
resp2.setHeader("Date", formatDate(now));
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
}
@Test
public void responseToGetWithQueryFrom1_0OriginVia1_1ProxyIsNotCached()
throws Exception {
HttpRequest req1 = new HttpGet("http://foo.example.com/bar?baz=quux");
HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
resp1.setEntity(HttpTestUtils.makeBody(200));
resp1.setHeader("Content-Length","200");
resp1.setHeader("Date", formatDate(now));
resp1.setHeader("Expires", formatDate(tenSecondsFromNow));
resp1.setHeader("Via","1.0 someproxy");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new HttpGet("http://foo.example.com/bar?baz=quux");
HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_OK, "OK");
resp2.setEntity(HttpTestUtils.makeBody(200));
resp2.setHeader("Content-Length","200");
resp2.setHeader("Date", formatDate(now));
resp2.setHeader("Via","1.0 someproxy");
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
verifyMocks();
}
/*
* "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 {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("FROB", "/", HttpVersion.HTTP_1_1);
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
HttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("ETag", "\"etag\"");
backendExpectsAnyRequest().andReturn(resp3);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
HttpResponse result = impl.execute(host, req3);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp3, result));
}
@Test
public void shouldInvalidateAllVariantsForUnknownMethod()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req1.setHeader("User-Agent", "agent1");
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
resp1.setHeader("Vary", "User-Agent");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req2.setHeader("User-Agent", "agent2");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Cache-Control","max-age=3600");
resp2.setHeader("Vary", "User-Agent");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = new BasicHttpRequest("FROB", "/", HttpVersion.HTTP_1_1);
req3.setHeader("User-Agent", "agent3");
HttpResponse resp3 = HttpTestUtils.make200Response();
resp3.setHeader("Cache-Control","max-age=3600");
backendExpectsAnyRequest().andReturn(resp3);
HttpRequest req4 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req4.setHeader("User-Agent", "agent1");
HttpResponse resp4 = HttpTestUtils.make200Response();
resp4.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp4);
HttpRequest req5 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
req5.setHeader("User-Agent", "agent2");
HttpResponse resp5 = HttpTestUtils.make200Response();
resp5.setHeader("ETag", "\"etag2\"");
backendExpectsAnyRequest().andReturn(resp5);
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
impl.execute(host, req3);
HttpResponse result4 = impl.execute(host, req4);
HttpResponse result5 = impl.execute(host, req5);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp4, result4));
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 {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Cache-Control", "max-age=3600");
resp1.setHeader("ETag", "\"etag1\"");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-age=0");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("Date", formatDate(now));
resp2.setHeader("Cache-Control", "max-age=3600");
resp2.setHeader("ETag", "\"etag2\"");
backendExpectsAnyRequest().andReturn(resp2);
HttpRequest req3 = HttpTestUtils.makeDefaultRequest();
replayMocks();
impl.execute(host, req1);
impl.execute(host, req2);
HttpResponse result = impl.execute(host, req3);
verifyMocks();
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 {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(now));
resp1.setHeader("Expires", formatDate(now));
resp1.removeHeaders("Cache-Control");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"etag2\"");
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
@Test
public void expiresPriorToDateWithNoCacheControlIsNotCacheable()
throws Exception {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(now));
resp1.setHeader("Expires", formatDate(tenSecondsAgo));
resp1.removeHeaders("Cache-Control");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=1000");
HttpResponse resp2 = HttpTestUtils.make200Response();
resp2.setHeader("ETag", "\"etag2\"");
backendExpectsAnyRequest().andReturn(resp2);
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp2, result));
}
/*
* "If a request includes the no-cache directive, it SHOULD NOT
* include min-fresh, max-stale, or max-age."
*
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
*/
@Test
public void otherFreshnessRequestDirectivesNotAllowedWithNoCache()
throws Exception {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
req1.setHeader("Cache-Control", "min-fresh=10, no-cache");
req1.addHeader("Cache-Control", "max-stale=0, max-age=0");
Capture cap = new Capture();
expect(mockBackend.execute(same(host), capture(cap), (HttpContext)isNull()))
.andReturn(HttpTestUtils.make200Response());
replayMocks();
impl.execute(host, req1);
verifyMocks();
HttpRequest captured = cap.getValue();
boolean foundNoCache = false;
boolean foundDisallowedDirective = false;
List disallowed =
Arrays.asList("min-fresh", "max-stale", "max-age");
for(Header h : captured.getHeaders("Cache-Control")) {
for(HeaderElement elt : h.getElements()) {
if (disallowed.contains(elt.getName())) {
foundDisallowedDirective = true;
}
if ("no-cache".equals(elt.getName())) {
foundNoCache = true;
}
}
}
assertTrue(foundNoCache);
assertFalse(foundDisallowedDirective);
}
/*
* "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 {
HttpRequest req = HttpTestUtils.makeDefaultRequest();
req.setHeader("Cache-Control", "only-if-cached");
replayMocks();
HttpResponse result = impl.execute(host, req);
verifyMocks();
assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
result.getStatusLine().getStatusCode());
}
@Test
public void cacheHitOkWithOnlyIfCached()
throws Exception {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Cache-Control","max-age=3600");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "only-if-cached");
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
}
@Test
public void returns504ForStaleEntryWithOnlyIfCached()
throws Exception {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","max-age=5");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "only-if-cached");
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT,
result.getStatusLine().getStatusCode());
}
@Test
public void returnsStaleCacheEntryWithOnlyIfCachedAndMaxStale()
throws Exception {
HttpRequest req1 = HttpTestUtils.makeDefaultRequest();
HttpResponse resp1 = HttpTestUtils.make200Response();
resp1.setHeader("Date", formatDate(tenSecondsAgo));
resp1.setHeader("Cache-Control","max-age=5");
backendExpectsAnyRequest().andReturn(resp1);
HttpRequest req2 = HttpTestUtils.makeDefaultRequest();
req2.setHeader("Cache-Control", "max-stale=20, only-if-cached");
replayMocks();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
assertTrue(HttpTestUtils.semanticallyTransparent(resp1, result));
}
}