/* * Copyright (C) 2011 The Android Open Source Project * * Licensed 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. */ package libcore.net.http; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.InvocationHandler; import java.net.CacheRequest; import java.net.CacheResponse; import java.net.CookieHandler; import java.net.CookieManager; import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.ResponseCache; import java.net.SecureCacheResponse; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.security.Principal; import java.security.cert.Certificate; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HttpsURLConnection; import junit.framework.TestCase; import libcore.javax.net.ssl.TestSSLContext; import tests.io.MockOs; public final class HttpResponseCacheTest extends TestCase { private MockWebServer server = new MockWebServer(); private HttpResponseCache cache; private final MockOs mockOs = new MockOs(); private final CookieManager cookieManager = new CookieManager(); @Override protected void setUp() throws Exception { super.setUp(); String tmp = System.getProperty("java.io.tmpdir"); File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); ResponseCache.setDefault(cache); mockOs.install(); CookieHandler.setDefault(cookieManager); } @Override protected void tearDown() throws Exception { mockOs.uninstall(); server.shutdown(); ResponseCache.setDefault(null); cache.getCache().delete(); CookieHandler.setDefault(null); super.tearDown(); } /** * Test that response caching is consistent with the RI and the spec. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 */ public void testResponseCachingByResponseCode() throws Exception { // Test each documented HTTP/1.1 code, plus the first unused value in each range. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // We can't test 100 because it's not really a response. // assertCached(false, 100); assertCached(false, 101); assertCached(false, 102); assertCached(true, 200); assertCached(false, 201); assertCached(false, 202); assertCached(true, 203); assertCached(false, 204); assertCached(false, 205); assertCached(false, 206); // we don't cache partial responses assertCached(false, 207); assertCached(true, 300); assertCached(true, 301); for (int i = 302; i <= 308; ++i) { assertCached(false, i); } for (int i = 400; i <= 406; ++i) { assertCached(false, i); } // (See test_responseCaching_407.) assertCached(false, 408); assertCached(false, 409); // (See test_responseCaching_410.) for (int i = 411; i <= 418; ++i) { assertCached(false, i); } for (int i = 500; i <= 506; ++i) { assertCached(false, i); } } /** * Response code 407 should only come from proxy servers. Android's client * throws if it is sent by an origin server. */ public void testOriginServerSends407() throws Exception { server.enqueue(new MockResponse().setResponseCode(407)); server.play(); URL url = server.getUrl("/"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); try { conn.getResponseCode(); fail(); } catch (IOException expected) { } } public void test_responseCaching_410() throws Exception { // the HTTP spec permits caching 410s, but the RI doesn't. assertCached(true, 410); } private void assertCached(boolean shouldPut, int responseCode) throws Exception { server = new MockWebServer(); MockResponse response = new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setResponseCode(responseCode) .setBody("ABCDE") .addHeader("WWW-Authenticate: challenge"); if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) { response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); } server.enqueue(response); server.play(); URL url = server.getUrl("/"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); assertEquals(responseCode, conn.getResponseCode()); // exhaust the content stream readAscii(conn); CacheResponse cached = cache.get(url.toURI(), "GET", Collections.<String, List<String>>emptyMap()); if (shouldPut) { assertNotNull(Integer.toString(responseCode), cached); cached.getBody().close(); } else { assertNull(Integer.toString(responseCode), cached); } server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers } /** * Test that we can interrogate the response when the cache is being * populated. http://code.google.com/p/android/issues/detail?id=7787 */ public void testResponseCacheCallbackApis() throws Exception { final String body = "ABCDE"; final AtomicInteger cacheCount = new AtomicInteger(); server.enqueue(new MockResponse() .setStatus("HTTP/1.1 200 Fantastic") .addHeader("fgh: ijk") .setBody(body)); server.play(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) throws IOException { return null; } @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { HttpURLConnection httpConnection = (HttpURLConnection) conn; try { httpConnection.getRequestProperties(); fail(); } catch (IllegalStateException expected) { } try { httpConnection.addRequestProperty("K", "V"); fail(); } catch (IllegalStateException expected) { } assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null)); assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), httpConnection.getHeaderFields().get(null)); assertEquals(200, httpConnection.getResponseCode()); assertEquals("Fantastic", httpConnection.getResponseMessage()); assertEquals(body.length(), httpConnection.getContentLength()); assertEquals("ijk", httpConnection.getHeaderField("fgh")); try { httpConnection.getInputStream(); // the RI doesn't forbid this, but it should fail(); } catch (IOException expected) { } cacheCount.incrementAndGet(); return null; } }); URL url = server.getUrl("/"); URLConnection connection = url.openConnection(); assertEquals(body, readAscii(connection)); assertEquals(1, cacheCount.get()); } public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException { testResponseCaching(TransferKind.FIXED_LENGTH); } public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { testResponseCaching(TransferKind.CHUNKED); } public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { testResponseCaching(TransferKind.END_OF_STREAM); } /** * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption * http://code.google.com/p/android/issues/detail?id=8175 */ private void testResponseCaching(TransferKind transferKind) throws IOException { MockResponse response = new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setStatus("HTTP/1.1 200 Fantastic"); transferKind.setBody(response, "I love puppies but hate spiders", 1); server.enqueue(response); server.play(); // Make sure that calling skip() doesn't omit bytes from the cache. HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = urlConnection.getInputStream(); assertEquals("I love ", readAscii(urlConnection, "I love ".length())); reliableSkip(in, "puppies but hate ".length()); assertEquals("spiders", readAscii(urlConnection, "spiders".length())); assertEquals(-1, in.read()); in.close(); assertEquals(1, cache.getWriteSuccessCount()); assertEquals(0, cache.getWriteAbortCount()); urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // cached! in = urlConnection.getInputStream(); assertEquals("I love puppies but hate spiders", readAscii(urlConnection, "I love puppies but hate spiders".length())); assertEquals(200, urlConnection.getResponseCode()); assertEquals("Fantastic", urlConnection.getResponseMessage()); assertEquals(-1, in.read()); in.close(); assertEquals(1, cache.getWriteSuccessCount()); assertEquals(0, cache.getWriteAbortCount()); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getHitCount()); } public void testSecureResponseCaching() throws IOException { TestSSLContext testSSLContext = TestSSLContext.create(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setBody("ABC")); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("ABC", readAscii(connection)); // OpenJDK 6 fails on this line, complaining that the connection isn't open yet String suite = connection.getCipherSuite(); List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates()); List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates()); Principal peerPrincipal = connection.getPeerPrincipal(); Principal localPrincipal = connection.getLocalPrincipal(); connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached! connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("ABC", readAscii(connection)); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(1, cache.getHitCount()); assertEquals(suite, connection.getCipherSuite()); assertEquals(localCerts, toListOrNull(connection.getLocalCertificates())); assertEquals(serverCerts, toListOrNull(connection.getServerCertificates())); assertEquals(peerPrincipal, connection.getPeerPrincipal()); assertEquals(localPrincipal, connection.getLocalPrincipal()); } public void testCacheReturnsInsecureResponseForSecureRequest() throws IOException { TestSSLContext testSSLContext = TestSSLContext.create(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse().setBody("ABC")); server.enqueue(new MockResponse().setBody("DEF")); server.play(); ResponseCache.setDefault(new InsecureResponseCache()); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("ABC", readAscii(connection)); connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // not cached! connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("DEF", readAscii(connection)); } public void testResponseCachingAndRedirects() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo")); server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setBody("ABC")); server.enqueue(new MockResponse().setBody("DEF")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("ABC", readAscii(connection)); connection = server.getUrl("/").openConnection(); // cached! assertEquals("ABC", readAscii(connection)); assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects assertEquals(2, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } public void testRedirectToCachedResult() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .setBody("ABC")); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo")); server.enqueue(new MockResponse().setBody("DEF")); server.play(); assertEquals("ABC", readAscii(server.getUrl("/foo").openConnection())); RecordedRequest request1 = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); assertEquals(0, request1.getSequenceNumber()); assertEquals("ABC", readAscii(server.getUrl("/bar").openConnection())); RecordedRequest request2 = server.takeRequest(); assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); assertEquals(1, request2.getSequenceNumber()); // an unrelated request should reuse the pooled connection assertEquals("DEF", readAscii(server.getUrl("/baz").openConnection())); RecordedRequest request3 = server.takeRequest(); assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); assertEquals(2, request3.getSequenceNumber()); } public void testSecureResponseCachingAndRedirects() throws IOException { TestSSLContext testSSLContext = TestSSLContext.create(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo")); server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setBody("ABC")); server.enqueue(new MockResponse().setBody("DEF")); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("ABC", readAscii(connection)); connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached! connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertEquals("ABC", readAscii(connection)); assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 assertEquals(2, cache.getHitCount()); } public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException { server.enqueue(new MockResponse().setBody("ABC")); server.play(); final AtomicReference<Map<String, List<String>>> requestHeadersRef = new AtomicReference<Map<String, List<String>>>(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) throws IOException { requestHeadersRef.set(requestHeaders); return null; } @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { return null; } }); URL url = server.getUrl("/"); URLConnection urlConnection = url.openConnection(); urlConnection.addRequestProperty("A", "android"); readAscii(urlConnection); assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); } public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException { testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); } public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException { testServerPrematureDisconnect(TransferKind.CHUNKED); } public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { /* * Intentionally empty. This case doesn't make sense because there's no * such thing as a premature disconnect when the disconnect itself * indicates the end of the data stream. */ } private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { MockResponse response = new MockResponse(); transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); server.enqueue(truncateViolently(response, 16)); server.enqueue(new MockResponse().setBody("Request #2")); server.play(); BufferedReader reader = new BufferedReader(new InputStreamReader( server.getUrl("/").openConnection().getInputStream())); assertEquals("ABCDE", reader.readLine()); try { reader.readLine(); fail("This implementation silently ignored a truncated HTTP body."); } catch (IOException expected) { } finally { reader.close(); } assertEquals(1, cache.getWriteAbortCount()); assertEquals(0, cache.getWriteSuccessCount()); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("Request #2", readAscii(connection)); assertEquals(1, cache.getWriteAbortCount()); assertEquals(1, cache.getWriteSuccessCount()); } public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException { testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); } public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException { testClientPrematureDisconnect(TransferKind.CHUNKED); } public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException { testClientPrematureDisconnect(TransferKind.END_OF_STREAM); } private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { MockResponse response = new MockResponse(); transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); server.enqueue(response); server.enqueue(new MockResponse().setBody("Request #2")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals("ABCDE", readAscii(connection, 5)); in.close(); try { in.read(); fail("Expected an IOException because the stream is closed."); } catch (IOException expected) { } assertEquals(1, cache.getWriteAbortCount()); assertEquals(0, cache.getWriteSuccessCount()); connection = server.getUrl("/").openConnection(); assertEquals("Request #2", readAscii(connection)); assertEquals(1, cache.getWriteAbortCount()); assertEquals(1, cache.getWriteSuccessCount()); } public void testDefaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { // last modified: 105 seconds ago // served: 5 seconds ago // default lifetime: (105 - 5) / 10 = 10 seconds // expires: 10 seconds from served date = 5 seconds from now server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) .setBody("A")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); URLConnection connection = url.openConnection(); assertEquals("A", readAscii(connection)); assertNull(connection.getHeaderField("Warning")); } public void testDefaultExpirationDateConditionallyCached() throws Exception { // last modified: 115 seconds ago // served: 15 seconds ago // default lifetime: (115 - 15) / 10 = 10 seconds // expires: 10 seconds from served date = 5 seconds ago String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testDefaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { // last modified: 105 days ago // served: 5 days ago // default lifetime: (105 - 5) / 10 = 10 days // expires: 10 days from served date = 5 days from now server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) .setBody("A")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection)); assertEquals("113 HttpURLConnection \"Heuristic expiration\"", connection.getHeaderField("Warning")); } public void testNoDefaultExpirationForUrlsWithQueryString() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/?foo=bar"); assertEquals("A", readAscii(url.openConnection())); assertEquals("B", readAscii(url.openConnection())); } public void testExpirationDateInThePastWithLastModifiedHeader() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testExpirationDateInThePastWithNoLastModifiedHeader() throws Exception { assertNotCached(new MockResponse() .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); } public void testExpirationDateInTheFuture() throws Exception { assertFullyCached(new MockResponse() .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); } public void testMaxAgePreferredWithMaxAgeAndExpires() throws Exception { assertFullyCached(new MockResponse() .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=60")); } public void testMaxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Cache-Control: max-age=60")); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testMaxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { /* * Chrome interprets max-age relative to the local clock. Both our cache * and Firefox both use the earlier of the local and server's clock. */ assertNotCached(new MockResponse() .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60")); } public void testMaxAgeInTheFutureWithDateHeader() throws Exception { assertFullyCached(new MockResponse() .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=60")); } public void testMaxAgeInTheFutureWithNoDateHeader() throws Exception { assertFullyCached(new MockResponse() .addHeader("Cache-Control: max-age=60")); } public void testMaxAgeWithLastModifiedButNoServedDate() throws Exception { assertFullyCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60")); } public void testMaxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { assertFullyCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60")); } public void testMaxAgePreferredOverLowerSharedMaxAge() throws Exception { assertFullyCached(new MockResponse() .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: s-maxage=60") .addHeader("Cache-Control: max-age=180")); } public void testMaxAgePreferredOverHigherMaxAge() throws Exception { assertNotCached(new MockResponse() .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: s-maxage=180") .addHeader("Cache-Control: max-age=60")); } public void testRequestMethodOptionsIsNotCached() throws Exception { testRequestMethod("OPTIONS", false); } public void testRequestMethodGetIsCached() throws Exception { testRequestMethod("GET", true); } public void testRequestMethodHeadIsNotCached() throws Exception { // We could support this but choose not to for implementation simplicity testRequestMethod("HEAD", false); } public void testRequestMethodPostIsNotCached() throws Exception { // We could support this but choose not to for implementation simplicity testRequestMethod("POST", false); } public void testRequestMethodPutIsNotCached() throws Exception { testRequestMethod("PUT", false); } public void testRequestMethodDeleteIsNotCached() throws Exception { testRequestMethod("DELETE", false); } public void testRequestMethodTraceIsNotCached() throws Exception { testRequestMethod("TRACE", false); } private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { /* * 1. seed the cache (potentially) * 2. expect a cache hit or miss */ server.enqueue(new MockResponse() .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("X-Response-ID: 1")); server.enqueue(new MockResponse() .addHeader("X-Response-ID: 2")); server.play(); URL url = server.getUrl("/"); HttpURLConnection request1 = (HttpURLConnection) url.openConnection(); request1.setRequestMethod(requestMethod); addRequestBodyIfNecessary(requestMethod, request1); assertEquals("1", request1.getHeaderField("X-Response-ID")); URLConnection request2 = url.openConnection(); if (expectCached) { assertEquals("1", request1.getHeaderField("X-Response-ID")); } else { assertEquals("2", request2.getHeaderField("X-Response-ID")); } } public void testPostInvalidatesCache() throws Exception { testMethodInvalidates("POST"); } public void testPutInvalidatesCache() throws Exception { testMethodInvalidates("PUT"); } public void testDeleteMethodInvalidatesCache() throws Exception { testMethodInvalidates("DELETE"); } private void testMethodInvalidates(String requestMethod) throws Exception { /* * 1. seed the cache * 2. invalidate it * 3. expect a cache miss */ server.enqueue(new MockResponse().setBody("A") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); server.enqueue(new MockResponse().setBody("B")); server.enqueue(new MockResponse().setBody("C")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); HttpURLConnection invalidate = (HttpURLConnection) url.openConnection(); invalidate.setRequestMethod(requestMethod); addRequestBodyIfNecessary(requestMethod, invalidate); assertEquals("B", readAscii(invalidate)); assertEquals("C", readAscii(url.openConnection())); } public void testEtag() throws Exception { RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("ETag: v1")); assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); } public void testEtagAndExpirationDateInThePast() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("ETag: v1") .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-None-Match: v1")); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testEtagAndExpirationDateInTheFuture() throws Exception { assertFullyCached(new MockResponse() .addHeader("ETag: v1") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); } public void testCacheControlNoCache() throws Exception { assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); } public void testCacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Cache-Control: no-cache")); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testPragmaNoCache() throws Exception { assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); } public void testPragmaNoCacheAndExpirationDateInTheFuture() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Pragma: no-cache")); List<String> headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } public void testCacheControlNoStore() throws Exception { assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); } public void testCacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { assertNotCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Cache-Control: no-store")); } public void testPartialRangeResponsesDoNotCorruptCache() throws Exception { /* * 1. request a range * 2. request a full document, expecting a cache miss */ server.enqueue(new MockResponse().setBody("AA") .setResponseCode(HttpURLConnection.HTTP_PARTIAL) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Content-Range: bytes 1000-1001/2000")); server.enqueue(new MockResponse().setBody("BB")); server.play(); URL url = server.getUrl("/"); URLConnection range = url.openConnection(); range.addRequestProperty("Range", "bytes=1000-1001"); assertEquals("AA", readAscii(range)); assertEquals("BB", readAscii(url.openConnection())); } public void testServerReturnsDocumentOlderThanCache() throws Exception { server.enqueue(new MockResponse().setBody("A") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); server.enqueue(new MockResponse().setBody("B") .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); assertEquals("A", readAscii(url.openConnection())); } public void testNonIdentityEncodingAndConditionalCache() throws Exception { assertNonIdentityEncodingCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); } public void testNonIdentityEncodingAndFullCache() throws Exception { assertNonIdentityEncodingCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); } private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { server.enqueue(response .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) .addHeader("Content-Encoding: gzip")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection())); assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection())); } public void testExpiresDateBeforeModifiedDate() throws Exception { assertConditionallyCached(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); } public void testRequestMaxAge() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "max-age=30"); assertEquals("B", readAscii(connection)); } public void testRequestMinFresh() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Cache-Control: max-age=60") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "min-fresh=120"); assertEquals("B", readAscii(connection)); } public void testRequestMaxStale() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Cache-Control: max-age=120") .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "max-stale=180"); assertEquals("A", readAscii(connection)); assertEquals("110 HttpURLConnection \"Response is stale\"", connection.getHeaderField("Warning")); } public void testRequestMaxStaleNotHonoredWithMustRevalidate() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Cache-Control: max-age=120, must-revalidate") .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "max-stale=180"); assertEquals("B", readAscii(connection)); } public void testRequestOnlyIfCachedWithNoResponseCached() throws IOException { // (no responses enqueued) server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "only-if-cached"); assertGatewayTimeout(connection); } public void testRequestOnlyIfCachedWithFullResponseCached() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "only-if-cached"); assertEquals("A", readAscii(server.getUrl("/").openConnection())); } public void testRequestOnlyIfCachedWithConditionalResponseCached() throws IOException { server.enqueue(new MockResponse().setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "only-if-cached"); assertGatewayTimeout(connection); } public void testRequestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { server.enqueue(new MockResponse().setBody("A")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.addRequestProperty("Cache-Control", "only-if-cached"); assertGatewayTimeout(connection); } public void testRequestCacheControlNoCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); URLConnection connection = url.openConnection(); connection.setRequestProperty("Cache-Control", "no-cache"); assertEquals("B", readAscii(connection)); } public void testRequestPragmaNoCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); URLConnection connection = url.openConnection(); connection.setRequestProperty("Pragma", "no-cache"); assertEquals("B", readAscii(connection)); } public void testClientSuppliedIfModifiedSinceWithCachedResult() throws Exception { MockResponse response = new MockResponse() .addHeader("ETag: v3") .addHeader("Cache-Control: max-age=0"); String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); RecordedRequest request = assertClientSuppliedCondition( response, "If-Modified-Since", ifModifiedSinceDate); List<String> headers = request.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); assertFalse(headers.contains("If-None-Match: v3")); } public void testClientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); MockResponse response = new MockResponse() .addHeader("Last-Modified: " + lastModifiedDate) .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: max-age=0"); RecordedRequest request = assertClientSuppliedCondition( response, "If-None-Match", "v1"); List<String> headers = request.getHeaders(); assertTrue(headers.contains("If-None-Match: v1")); assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); } private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, String conditionValue) throws Exception { server.enqueue(seed.setBody("A")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.addRequestProperty(conditionName, conditionValue); assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); assertEquals("", readAscii(connection)); server.takeRequest(); // seed return server.takeRequest(); } public void testSetIfModifiedSince() throws Exception { Date since = new Date(); server.enqueue(new MockResponse().setBody("A")); server.play(); URL url = server.getUrl("/"); URLConnection connection = url.openConnection(); connection.setIfModifiedSince(since.getTime()); assertEquals("A", readAscii(connection)); RecordedRequest request = server.takeRequest(); assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since))); } public void testClientSuppliedConditionWithoutCachedResult() throws Exception { server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); assertEquals("", readAscii(connection)); } public void testAuthorizationRequestHeaderPreventsCaching() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: max-age=60") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection = url.openConnection(); connection.addRequestProperty("Authorization", "password"); assertEquals("A", readAscii(connection)); assertEquals("B", readAscii(url.openConnection())); } public void testAuthorizationResponseCachedWithSMaxAge() throws Exception { assertAuthorizationRequestFullyCached(new MockResponse() .addHeader("Cache-Control: s-maxage=60")); } public void testAuthorizationResponseCachedWithPublic() throws Exception { assertAuthorizationRequestFullyCached(new MockResponse() .addHeader("Cache-Control: public")); } public void testAuthorizationResponseCachedWithMustRevalidate() throws Exception { assertAuthorizationRequestFullyCached(new MockResponse() .addHeader("Cache-Control: must-revalidate")); } public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { server.enqueue(response .addHeader("Cache-Control: max-age=60") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection = url.openConnection(); connection.addRequestProperty("Authorization", "password"); assertEquals("A", readAscii(connection)); assertEquals("A", readAscii(url.openConnection())); } public void testContentLocationDoesNotPopulateCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Content-Location: /bar") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/foo").openConnection())); assertEquals("B", readAscii(server.getUrl("/bar").openConnection())); } public void testUseCachesFalseDoesNotWriteToCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .setBody("A").setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URLConnection connection = server.getUrl("/").openConnection(); connection.setUseCaches(false); assertEquals("A", readAscii(connection)); assertEquals("B", readAscii(server.getUrl("/").openConnection())); } public void testUseCachesFalseDoesNotReadFromCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .setBody("A").setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection connection = server.getUrl("/").openConnection(); connection.setUseCaches(false); assertEquals("B", readAscii(connection)); } public void testDefaultUseCachesSetsInitialValueOnly() throws Exception { URL url = new URL("http://localhost/"); URLConnection c1 = url.openConnection(); URLConnection c2 = url.openConnection(); assertTrue(c1.getDefaultUseCaches()); c1.setDefaultUseCaches(false); try { assertTrue(c1.getUseCaches()); assertTrue(c2.getUseCaches()); URLConnection c3 = url.openConnection(); assertFalse(c3.getUseCaches()); } finally { c1.setDefaultUseCaches(true); } } public void testConnectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/a").openConnection())); assertEquals("A", readAscii(server.getUrl("/a").openConnection())); assertEquals("B", readAscii(server.getUrl("/b").openConnection())); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); assertEquals(2, server.takeRequest().getSequenceNumber()); } public void testStatisticsConditionalCacheMiss() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.enqueue(new MockResponse().setBody("C")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); assertEquals("B", readAscii(server.getUrl("/").openConnection())); assertEquals("C", readAscii(server.getUrl("/").openConnection())); assertEquals(3, cache.getRequestCount()); assertEquals(3, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); } public void testStatisticsConditionalCacheHit() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals(3, cache.getRequestCount()); assertEquals(3, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } public void testStatisticsFullCacheHit() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .setBody("A")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals(3, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } public void testVaryMatchesChangedRequestHeaderField() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); HttpURLConnection frConnection = (HttpURLConnection) url.openConnection(); frConnection.addRequestProperty("Accept-Language", "fr-CA"); assertEquals("A", readAscii(frConnection)); HttpURLConnection enConnection = (HttpURLConnection) url.openConnection(); enConnection.addRequestProperty("Accept-Language", "en-US"); assertEquals("B", readAscii(enConnection)); } public void testVaryMatchesUnchangedRequestHeaderField() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection1 = url.openConnection(); connection1.addRequestProperty("Accept-Language", "fr-CA"); assertEquals("A", readAscii(connection1)); URLConnection connection2 = url.openConnection(); connection2.addRequestProperty("Accept-Language", "fr-CA"); assertEquals("A", readAscii(connection2)); } public void testVaryMatchesAbsentRequestHeaderField() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals("A", readAscii(server.getUrl("/").openConnection())); } public void testVaryMatchesAddedRequestHeaderField() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); URLConnection fooConnection = server.getUrl("/").openConnection(); fooConnection.addRequestProperty("Foo", "bar"); assertEquals("B", readAscii(fooConnection)); } public void testVaryMatchesRemovedRequestHeaderField() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URLConnection fooConnection = server.getUrl("/").openConnection(); fooConnection.addRequestProperty("Foo", "bar"); assertEquals("A", readAscii(fooConnection)); assertEquals("B", readAscii(server.getUrl("/").openConnection())); } public void testVaryFieldsAreCaseInsensitive() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: ACCEPT-LANGUAGE") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection1 = url.openConnection(); connection1.addRequestProperty("Accept-Language", "fr-CA"); assertEquals("A", readAscii(connection1)); URLConnection connection2 = url.openConnection(); connection2.addRequestProperty("accept-language", "fr-CA"); assertEquals("A", readAscii(connection2)); } public void testVaryMultipleFieldsWithMatch() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language, Accept-Charset") .addHeader("Vary: Accept-Encoding") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection1 = url.openConnection(); connection1.addRequestProperty("Accept-Language", "fr-CA"); connection1.addRequestProperty("Accept-Charset", "UTF-8"); connection1.addRequestProperty("Accept-Encoding", "identity"); assertEquals("A", readAscii(connection1)); URLConnection connection2 = url.openConnection(); connection2.addRequestProperty("Accept-Language", "fr-CA"); connection2.addRequestProperty("Accept-Charset", "UTF-8"); connection2.addRequestProperty("Accept-Encoding", "identity"); assertEquals("A", readAscii(connection2)); } public void testVaryMultipleFieldsWithNoMatch() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language, Accept-Charset") .addHeader("Vary: Accept-Encoding") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection frConnection = url.openConnection(); frConnection.addRequestProperty("Accept-Language", "fr-CA"); frConnection.addRequestProperty("Accept-Charset", "UTF-8"); frConnection.addRequestProperty("Accept-Encoding", "identity"); assertEquals("A", readAscii(frConnection)); URLConnection enConnection = url.openConnection(); enConnection.addRequestProperty("Accept-Language", "en-CA"); enConnection.addRequestProperty("Accept-Charset", "UTF-8"); enConnection.addRequestProperty("Accept-Encoding", "identity"); assertEquals("B", readAscii(enConnection)); } public void testVaryMultipleFieldValuesWithMatch() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection1 = url.openConnection(); connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); connection1.addRequestProperty("Accept-Language", "en-US"); assertEquals("A", readAscii(connection1)); URLConnection connection2 = url.openConnection(); connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); connection2.addRequestProperty("Accept-Language", "en-US"); assertEquals("A", readAscii(connection2)); } public void testVaryMultipleFieldValuesWithNoMatch() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); URLConnection connection1 = url.openConnection(); connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); connection1.addRequestProperty("Accept-Language", "en-US"); assertEquals("A", readAscii(connection1)); URLConnection connection2 = url.openConnection(); connection2.addRequestProperty("Accept-Language", "fr-CA"); connection2.addRequestProperty("Accept-Language", "en-US"); assertEquals("B", readAscii(connection2)); } public void testVaryAsterisk() throws Exception { server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: *") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); assertEquals("A", readAscii(server.getUrl("/").openConnection())); assertEquals("B", readAscii(server.getUrl("/").openConnection())); } public void testVaryAndHttps() throws Exception { TestSSLContext testSSLContext = TestSSLContext.create(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); HttpsURLConnection connection1 = (HttpsURLConnection) url.openConnection(); connection1.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection1.addRequestProperty("Accept-Language", "en-US"); assertEquals("A", readAscii(connection1)); HttpsURLConnection connection2 = (HttpsURLConnection) url.openConnection(); connection2.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection2.addRequestProperty("Accept-Language", "en-US"); assertEquals("A", readAscii(connection2)); } public void testDiskWriteFailureCacheDegradation() throws Exception { Deque<InvocationHandler> writeHandlers = mockOs.getHandlers("write"); int i = 0; boolean hasMoreScenarios = true; while (hasMoreScenarios) { mockOs.enqueueNormal("write", i++); mockOs.enqueueFault("write"); exercisePossiblyFaultyCache(false); hasMoreScenarios = writeHandlers.isEmpty(); writeHandlers.clear(); } System.out.println("Exercising the cache performs " + (i - 1) + " writes."); } public void testDiskReadFailureCacheDegradation() throws Exception { Deque<InvocationHandler> readHandlers = mockOs.getHandlers("read"); int i = 0; boolean hasMoreScenarios = true; while (hasMoreScenarios) { mockOs.enqueueNormal("read", i++); mockOs.enqueueFault("read"); exercisePossiblyFaultyCache(true); hasMoreScenarios = readHandlers.isEmpty(); readHandlers.clear(); } System.out.println("Exercising the cache performs " + (i - 1) + " reads."); } public void testCachePlusCookies() throws Exception { server.enqueue(new MockResponse() .addHeader("Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); assertCookies(url, "a=FIRST"); assertEquals("A", readAscii(url.openConnection())); assertCookies(url, "a=SECOND"); } public void testGetHeadersReturnsNetworkEndToEndHeaders() throws Exception { server.enqueue(new MockResponse() .addHeader("Allow: GET, HEAD") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .addHeader("Allow: GET, HEAD, PUT") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URLConnection connection1 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection1)); assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); URLConnection connection2 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection2)); assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); } public void testGetHeadersReturnsCachedHopByHopHeaders() throws Exception { server.enqueue(new MockResponse() .addHeader("Transfer-Encoding: identity") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .addHeader("Transfer-Encoding: none") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URLConnection connection1 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection1)); assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); URLConnection connection2 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection2)); assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); } public void testGetHeadersDeletesCached100LevelWarnings() throws Exception { server.enqueue(new MockResponse() .addHeader("Warning: 199 test danger") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URLConnection connection1 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection1)); assertEquals("199 test danger", connection1.getHeaderField("Warning")); URLConnection connection2 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection2)); assertEquals(null, connection2.getHeaderField("Warning")); } public void testGetHeadersRetainsCached200LevelWarnings() throws Exception { server.enqueue(new MockResponse() .addHeader("Warning: 299 test danger") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); URLConnection connection1 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection1)); assertEquals("299 test danger", connection1.getHeaderField("Warning")); URLConnection connection2 = server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection2)); assertEquals("299 test danger", connection2.getHeaderField("Warning")); } public void assertCookies(URL url, String... expectedCookies) throws Exception { List<String> actualCookies = new ArrayList<String>(); for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { actualCookies.add(cookie.toString()); } assertEquals(Arrays.asList(expectedCookies), actualCookies); } public void testCachePlusRange() throws Exception { assertNotCached(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_PARTIAL) .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Content-Range: bytes 100-100/200") .addHeader("Cache-Control: max-age=60")); } public void testConditionalHitUpdatesCache() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=0") .setBody("A")); server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=30") .addHeader("Allow: GET, HEAD") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.enqueue(new MockResponse().setBody("B")); server.play(); // cache miss; seed the cache HttpURLConnection connection1 = (HttpURLConnection) server.getUrl("/a").openConnection(); assertEquals("A", readAscii(connection1)); assertEquals(null, connection1.getHeaderField("Allow")); // conditional cache hit; update the cache HttpURLConnection connection2 = (HttpURLConnection) server.getUrl("/a").openConnection(); assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); assertEquals("A", readAscii(connection2)); assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); // full cache hit HttpURLConnection connection3 = (HttpURLConnection) server.getUrl("/a").openConnection(); assertEquals("A", readAscii(connection3)); assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); assertEquals(2, server.getRequestCount()); } /** * @param delta the offset from the current date to use. Negative * values yield dates in the past; positive values yield dates in the * future. */ private String formatDate(long delta, TimeUnit timeUnit) { return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); } private String formatDate(Date date) { DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); return rfc1123.format(date); } private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) throws IOException { if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { invalidate.setDoOutput(true); OutputStream requestBody = invalidate.getOutputStream(); requestBody.write('x'); requestBody.close(); } } private void assertNotCached(MockResponse response) throws Exception { server.enqueue(response.setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); assertEquals("B", readAscii(url.openConnection())); } private void exercisePossiblyFaultyCache(boolean permitReadBodyFailures) throws Exception { server.shutdown(); server = new MockWebServer(); server.enqueue(new MockResponse() .addHeader("Cache-Control: max-age=60") .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/" + UUID.randomUUID()); assertEquals("A", readAscii(url.openConnection())); URLConnection connection = url.openConnection(); InputStream in = connection.getInputStream(); try { int bodyChar = in.read(); assertTrue(bodyChar == 'A' || bodyChar == 'B'); assertEquals(-1, in.read()); } catch (IOException e) { if (!permitReadBodyFailures) { throw e; } } } /** * @return the request with the conditional get headers. */ private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { // scenario 1: condition succeeds server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); // scenario 2: condition fails server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); server.play(); URL valid = server.getUrl("/valid"); HttpURLConnection connection1 = (HttpURLConnection) valid.openConnection(); assertEquals("A", readAscii(connection1)); assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); assertEquals("A-OK", connection1.getResponseMessage()); HttpURLConnection connection2 = (HttpURLConnection) valid.openConnection(); assertEquals("A", readAscii(connection2)); assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); assertEquals("A-OK", connection2.getResponseMessage()); URL invalid = server.getUrl("/invalid"); HttpURLConnection connection3 = (HttpURLConnection) invalid.openConnection(); assertEquals("B", readAscii(connection3)); assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); assertEquals("B-OK", connection3.getResponseMessage()); HttpURLConnection connection4 = (HttpURLConnection) invalid.openConnection(); assertEquals("C", readAscii(connection4)); assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); assertEquals("C-OK", connection4.getResponseMessage()); server.takeRequest(); // regular get return server.takeRequest(); // conditional get } private void assertFullyCached(MockResponse response) throws Exception { server.enqueue(response.setBody("A")); server.enqueue(response.setBody("B")); server.play(); URL url = server.getUrl("/"); assertEquals("A", readAscii(url.openConnection())); assertEquals("A", readAscii(url.openConnection())); } /** * Shortens the body of {@code response} but not the corresponding headers. * Only useful to test how clients respond to the premature conclusion of * the HTTP body. */ private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { response.setSocketPolicy(DISCONNECT_AT_END); List<String> headers = new ArrayList<String>(response.getHeaders()); response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); response.getHeaders().clear(); response.getHeaders().addAll(headers); return response; } /** * Reads {@code count} characters from the stream. If the stream is * exhausted before {@code count} characters can be read, the remaining * characters are returned and the stream is closed. */ private String readAscii(URLConnection connection, int count) throws IOException { HttpURLConnection httpConnection = (HttpURLConnection) connection; InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST ? connection.getInputStream() : httpConnection.getErrorStream(); StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { int value = in.read(); if (value == -1) { in.close(); break; } result.append((char) value); } return result.toString(); } private String readAscii(URLConnection connection) throws IOException { return readAscii(connection, Integer.MAX_VALUE); } private void reliableSkip(InputStream in, int length) throws IOException { while (length > 0) { length -= in.skip(length); } } private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { try { connection.getInputStream(); fail(); } catch (FileNotFoundException expected) { } assertEquals(504, connection.getResponseCode()); assertEquals(-1, connection.getErrorStream().read()); } enum TransferKind { CHUNKED() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException { response.setChunkedBody(content, chunkSize); } }, FIXED_LENGTH() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) { response.setBody(content); } }, END_OF_STREAM() { @Override void setBody(MockResponse response, byte[] content, int chunkSize) { response.setBody(content); response.setSocketPolicy(DISCONNECT_AT_END); for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) { if (h.next().startsWith("Content-Length:")) { h.remove(); break; } } } }; abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; void setBody(MockResponse response, String content, int chunkSize) throws IOException { setBody(response, content.getBytes("UTF-8"), chunkSize); } } private <T> List<T> toListOrNull(T[] arrayOrNull) { return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; } /** * Returns a gzipped copy of {@code bytes}. */ public byte[] gzip(byte[] bytes) throws IOException { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); OutputStream gzippedOut = new GZIPOutputStream(bytesOut); gzippedOut.write(bytes); gzippedOut.close(); return bytesOut.toByteArray(); } private class InsecureResponseCache extends ResponseCache { @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { return cache.put(uri, connection); } @Override public CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) throws IOException { final CacheResponse response = cache.get(uri, requestMethod, requestHeaders); if (response instanceof SecureCacheResponse) { return new CacheResponse() { @Override public InputStream getBody() throws IOException { return response.getBody(); } @Override public Map<String, List<String>> getHeaders() throws IOException { return response.getHeaders(); } }; } return response; } } }