From f80b1f3a4a6a5ba3fa0366e8de425c5e229b6c5d Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sat, 25 Oct 2014 11:42:10 -0400 Subject: [PATCH] Fix a bug where the response cache could be corrupted. When streaming a response, we copy data from our buffer to the cached file on disk. Unfortunately we were copying N bytes from the front of the buffer when we wanted N bytes from the back of the buffer. Typically these are the same, but certain access patterns can cause them to be different, corrpting the cached file on disk. This was uncovered by migrating the cache tests from operating on HttpURLConnection's API to our new API. --- .../java/com/squareup/okhttp/CacheTest.java | 1537 +++++++------- .../com/squareup/okhttp/OkHttpClientTest.java | 20 +- .../internal/http/URLConnectionTest.java | 34 +- .../okhttp/AbstractResponseCache.java | 0 .../okhttp/UrlConnectionCacheTest.java | 1864 +++++++++++++++++ .../okhttp/internal/huc/CacheAdapterTest.java | 0 .../internal/huc/JavaApiConverterTest.java | 37 +- .../internal/huc/ResponseCacheTest.java | 86 +- .../okhttp/internal/huc}/URLEncodingTest.java | 3 +- .../okhttp/internal/http/HttpConnection.java | 6 +- .../okhttp/internal/http/HttpEngine.java | 7 +- 11 files changed, 2749 insertions(+), 845 deletions(-) rename {okhttp-tests => okhttp-urlconnection}/src/test/java/com/squareup/okhttp/AbstractResponseCache.java (100%) create mode 100644 okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java rename {okhttp-tests => okhttp-urlconnection}/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java (100%) rename {okhttp-tests => okhttp-urlconnection}/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java (97%) rename {okhttp-tests => okhttp-urlconnection}/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java (95%) rename {okhttp-tests/src/test/java/com/squareup/okhttp/internal/http => okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc}/URLEncodingTest.java (98%) diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java index c898602adcf7..dcda4e99b8f5 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java @@ -22,24 +22,14 @@ import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import java.io.BufferedReader; 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.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.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; @@ -50,18 +40,14 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.TimeZone; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import okio.Buffer; import okio.BufferedSink; +import okio.BufferedSource; import okio.GzipSink; import okio.Okio; import org.junit.After; @@ -75,14 +61,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -/** - * Android's HttpResponseCacheTest. This tests both {@link Cache} and handling - * of {@link ResponseCache}. - */ +/** Test caching with {@link OkUrlFactory}. */ public final class CacheTest { private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { @@ -96,7 +78,7 @@ public final class CacheTest { @Rule public MockWebServerRule serverRule = new MockWebServerRule(); @Rule public MockWebServerRule server2Rule = new MockWebServerRule(); - private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient()); + private final OkHttpClient client = new OkHttpClient(); private MockWebServer server; private MockWebServer server2; private Cache cache; @@ -107,7 +89,7 @@ public final class CacheTest { server.setProtocolNegotiationEnabled(false); server2 = server2Rule.get(); cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE); - client.client().setCache(cache); + client.setCache(cache); CookieHandler.setDefault(cookieManager); } @@ -116,11 +98,6 @@ public final class CacheTest { CookieHandler.setDefault(null); } - @Test public void responseCacheAccessWithOkHttpMember() throws IOException { - assertSame(cache, client.client().getCache()); - assertNull(client.getResponseCache()); - } - /** * Test that response caching is consistent with the RI and the spec. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 @@ -169,28 +146,30 @@ public final class CacheTest { 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"); + MockResponse mockResponse = 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\""); + mockResponse.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); + mockResponse.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); } - server.enqueue(response); + server.enqueue(mockResponse); server.play(); - URL url = server.getUrl("/"); - HttpURLConnection conn = client.open(url); - assertEquals(responseCode, conn.getResponseCode()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .build(); + Response response = client.newCall(request).execute(); + assertEquals(responseCode, response.code()); - // exhaust the content stream - readAscii(conn); + // Exhaust the content stream. + response.body().string(); - Response cached = cache.get(new Request.Builder().url(url).build()); + Response cached = cache.get(request); if (shouldPut) { assertNotNull(Integer.toString(responseCode), cached); cached.body().close(); @@ -200,69 +179,6 @@ private void assertCached(boolean shouldPut, int responseCode) throws Exception 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 - */ - @Test public void responseCacheCallbackApis() throws Exception { - final String body = "ABCDE"; - final AtomicInteger cacheCount = new AtomicInteger(); - - server.enqueue(new MockResponse() - .setStatus("HTTP/1.1 200 Fantastic") - .addHeader("Content-Type: text/plain") - .addHeader("fgh: ijk") - .setBody(body)); - - client.setResponseCache(new AbstractResponseCache() { - @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { - HttpURLConnection httpURLConnection = (HttpURLConnection) connection; - assertEquals(server.getUrl("/"), uri.toURL()); - assertEquals(200, httpURLConnection.getResponseCode()); - try { - httpURLConnection.getInputStream(); - fail(); - } catch (UnsupportedOperationException expected) { - } - assertEquals("5", connection.getHeaderField("Content-Length")); - assertEquals("text/plain", connection.getHeaderField("Content-Type")); - assertEquals("ijk", connection.getHeaderField("fgh")); - cacheCount.incrementAndGet(); - return null; - } - }); - - URL url = server.getUrl("/"); - HttpURLConnection connection = client.open(url); - assertEquals(body, readAscii(connection)); - assertEquals(1, cacheCount.get()); - } - - /** Don't explode if the cache returns a null body. http://b/3373699 */ - @Test public void responseCacheReturnsNullOutputStream() throws Exception { - final AtomicBoolean aborted = new AtomicBoolean(); - client.setResponseCache(new AbstractResponseCache() { - @Override public CacheRequest put(URI uri, URLConnection connection) { - return new CacheRequest() { - @Override public void abort() { - aborted.set(true); - } - - @Override public OutputStream getBody() throws IOException { - return null; - } - }; - } - }); - - server.enqueue(new MockResponse().setBody("abcdef")); - - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("abc", readAscii(connection, 3)); - connection.getInputStream().close(); - assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here - } - @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { testResponseCaching(TransferKind.FIXED_LENGTH); } @@ -276,37 +192,39 @@ private void assertCached(boolean shouldPut, int responseCode) throws Exception } /** - * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption + * Skipping bytes in the input stream caused 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); + MockResponse mockResponse = new MockResponse() + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setStatus("HTTP/1.1 200 Fantastic"); + transferKind.setBody(mockResponse, "I love puppies but hate spiders", 1); + server.enqueue(mockResponse); // Make sure that calling skip() doesn't omit bytes from the cache. - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - 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(); + Request request = new Request.Builder().url(server.getUrl("/")).build(); + Response response1 = client.newCall(request).execute(); + + BufferedSource in1 = response1.body().source(); + assertEquals("I love ", in1.readUtf8("I love ".length())); + in1.skip("puppies but hate ".length()); + assertEquals("spiders", in1.readUtf8("spiders".length())); + assertTrue(in1.exhausted()); + in1.close(); assertEquals(1, cache.getWriteSuccessCount()); assertEquals(0, cache.getWriteAbortCount()); - urlConnection = client.open(server.getUrl("/")); // cached! - in = urlConnection.getInputStream(); + Response response2 = client.newCall(request).execute(); + BufferedSource in2 = response2.body().source(); assertEquals("I love puppies but hate spiders", - readAscii(urlConnection, "I love puppies but hate spiders".length())); - assertEquals(200, urlConnection.getResponseCode()); - assertEquals("Fantastic", urlConnection.getResponseMessage()); + in2.readUtf8("I love puppies but hate spiders".length())); + assertEquals(200, response2.code()); + assertEquals("Fantastic", response2.message()); - assertEquals(-1, in.read()); - in.close(); + assertTrue(in2.exhausted()); + in2.close(); assertEquals(1, cache.getWriteSuccessCount()); assertEquals(0, cache.getWriteAbortCount()); assertEquals(2, cache.getRequestCount()); @@ -315,36 +233,38 @@ private void testResponseCaching(TransferKind transferKind) throws IOException { @Test public void secureResponseCaching() throws IOException { server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + server.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setBody("ABC")); - HttpsURLConnection c1 = (HttpsURLConnection) client.open(server.getUrl("/")); - c1.setSSLSocketFactory(sslContext.getSocketFactory()); - c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(c1)); + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + + Request request = new Request.Builder().url(server.getUrl("/")).build(); + Response response1 = client.newCall(request).execute(); + BufferedSource in = response1.body().source(); + assertEquals("ABC", in.readUtf8()); // OpenJDK 6 fails on this line, complaining that the connection isn't open yet - String suite = c1.getCipherSuite(); - List localCerts = toListOrNull(c1.getLocalCertificates()); - List serverCerts = toListOrNull(c1.getServerCertificates()); - Principal peerPrincipal = c1.getPeerPrincipal(); - Principal localPrincipal = c1.getLocalPrincipal(); + String suite = response1.handshake().cipherSuite(); + List localCerts = response1.handshake().localCertificates(); + List serverCerts = response1.handshake().peerCertificates(); + Principal peerPrincipal = response1.handshake().peerPrincipal(); + Principal localPrincipal = response1.handshake().localPrincipal(); - HttpsURLConnection c2 = (HttpsURLConnection) client.open(server.getUrl("/")); // cached! - c2.setSSLSocketFactory(sslContext.getSocketFactory()); - c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(c2)); + Response response2 = client.newCall(request).execute(); // Cached! + assertEquals("ABC", response2.body().source().readUtf8()); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(1, cache.getHitCount()); - assertEquals(suite, c2.getCipherSuite()); - assertEquals(localCerts, toListOrNull(c2.getLocalCertificates())); - assertEquals(serverCerts, toListOrNull(c2.getServerCertificates())); - assertEquals(peerPrincipal, c2.getPeerPrincipal()); - assertEquals(localPrincipal, c2.getLocalPrincipal()); + assertEquals(suite, response2.handshake().cipherSuite()); + assertEquals(localCerts, response2.handshake().localCertificates()); + assertEquals(serverCerts, response2.handshake().peerCertificates()); + assertEquals(peerPrincipal, response2.handshake().peerPrincipal()); + assertEquals(localPrincipal, response2.handshake().localPrincipal()); } @Test public void responseCachingAndRedirects() throws Exception { @@ -352,16 +272,18 @@ private void testResponseCaching(TransferKind transferKind) throws IOException { .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)) + 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")); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("ABC", readAscii(connection)); + Request request = new Request.Builder().url(server.getUrl("/")).build(); + Response response1 = client.newCall(request).execute(); + assertEquals("ABC", response1.body().string()); - connection = client.open(server.getUrl("/")); // cached! - assertEquals("ABC", readAscii(connection)); + Response response2 = client.newCall(request).execute(); // Cached! + assertEquals("ABC", response2.body().string()); assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects assertEquals(2, cache.getNetworkCount()); @@ -370,53 +292,63 @@ private void testResponseCaching(TransferKind transferKind) throws IOException { @Test public void redirectToCachedResult() throws Exception { server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("ABC")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo")); - server.enqueue(new MockResponse().setBody("DEF")); - - assertEquals("ABC", readAscii(client.open(server.getUrl("/foo")))); - RecordedRequest request1 = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); - assertEquals(0, request1.getSequenceNumber()); - - assertEquals("ABC", readAscii(client.open(server.getUrl("/bar")))); - RecordedRequest request2 = server.takeRequest(); - assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); - assertEquals(1, request2.getSequenceNumber()); + server.enqueue(new MockResponse() + .setBody("DEF")); + + Request request1 = new Request.Builder().url(server.getUrl("/foo")).build(); + Response response1 = client.newCall(request1).execute(); + assertEquals("ABC", response1.body().string()); + RecordedRequest recordedRequest1 = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", recordedRequest1.getRequestLine()); + assertEquals(0, recordedRequest1.getSequenceNumber()); + + Request request2 = new Request.Builder().url(server.getUrl("/bar")).build(); + Response response2 = client.newCall(request2).execute(); + assertEquals("ABC", response2.body().string()); + RecordedRequest recordedRequest2 = server.takeRequest(); + assertEquals("GET /bar HTTP/1.1", recordedRequest2.getRequestLine()); + assertEquals(1, recordedRequest2.getSequenceNumber()); // an unrelated request should reuse the pooled connection - assertEquals("DEF", readAscii(client.open(server.getUrl("/baz")))); - RecordedRequest request3 = server.takeRequest(); - assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); - assertEquals(2, request3.getSequenceNumber()); + Request request3 = new Request.Builder().url(server.getUrl("/baz")).build(); + Response response3 = client.newCall(request3).execute(); + assertEquals("DEF", response3.body().string()); + RecordedRequest recordedRequest3 = server.takeRequest(); + assertEquals("GET /baz HTTP/1.1", recordedRequest3.getRequestLine()); + assertEquals(2, recordedRequest3.getSequenceNumber()); } @Test public void secureResponseCachingAndRedirects() throws IOException { server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + 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)) + 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")); - client.client().setSslSocketFactory(sslContext.getSocketFactory()); - client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); - assertEquals("ABC", readAscii(connection1)); - assertNotNull(connection1.getCipherSuite()); + Response response1 = get(server.getUrl("/")); + assertEquals("ABC", response1.body().string()); + assertNotNull(response1.handshake().cipherSuite()); // Cached! - HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); - assertEquals("ABC", readAscii(connection2)); - assertNotNull(connection2.getCipherSuite()); + Response response2 = get(server.getUrl("/")); + assertEquals("ABC", response2.body().string()); + assertNotNull(response2.handshake().cipherSuite()); assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 assertEquals(2, cache.getHitCount()); - assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite()); + assertEquals(response1.handshake().cipherSuite(), response2.handshake().cipherSuite()); } /** @@ -429,49 +361,32 @@ private void testResponseCaching(TransferKind transferKind) throws IOException { */ @Test public void secureResponseCachingAndProtocolRedirects() throws IOException { server2.useHttps(sslContext.getSocketFactory(), false); - server2.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + server2.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setBody("ABC")); server2.enqueue(new MockResponse().setBody("DEF")); - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + server.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: " + server2.getUrl("/"))); - client.client().setSslSocketFactory(sslContext.getSocketFactory()); - client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - HttpURLConnection connection1 = client.open(server.getUrl("/")); - assertEquals("ABC", readAscii(connection1)); + Response response1 = get(server.getUrl("/")); + assertEquals("ABC", response1.body().string()); // Cached! - HttpURLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("ABC", readAscii(connection2)); + Response response2 = get(server.getUrl("/")); + assertEquals("ABC", response2.body().string()); assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 assertEquals(2, cache.getHitCount()); } - @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException { - server.enqueue(new MockResponse().setBody("ABC")); - - final AtomicReference>> requestHeadersRef = new AtomicReference<>(); - client.setResponseCache(new AbstractResponseCache() { - @Override public CacheResponse get(URI uri, - String requestMethod, Map> requestHeaders) throws IOException { - requestHeadersRef.set(requestHeaders); - return null; - } - }); - - URL url = server.getUrl("/"); - URLConnection urlConnection = client.open(url); - urlConnection.addRequestProperty("A", "android"); - readAscii(urlConnection); - assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); - } - @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); } @@ -487,26 +402,26 @@ private void testResponseCaching(TransferKind transferKind) throws IOException { } 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")); - - BufferedReader reader = new BufferedReader( - new InputStreamReader(client.open(server.getUrl("/")).getInputStream())); - assertEquals("ABCDE", reader.readLine()); + MockResponse mockResponse = new MockResponse(); + transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); + server.enqueue(truncateViolently(mockResponse, 16)); + server.enqueue(new MockResponse() + .setBody("Request #2")); + + BufferedSource bodySource = get(server.getUrl("/")).body().source(); + assertEquals("ABCDE", bodySource.readUtf8Line()); try { - reader.readLine(); + bodySource.readUtf8Line(); fail("This implementation silently ignored a truncated HTTP body."); } catch (IOException expected) { } finally { - reader.close(); + bodySource.close(); } assertEquals(1, cache.getWriteAbortCount()); assertEquals(0, cache.getWriteSuccessCount()); - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("Request #2", readAscii(connection)); + Response response = get(server.getUrl("/")); + assertEquals("Request #2", response.body().string()); assertEquals(1, cache.getWriteAbortCount()); assertEquals(1, cache.getWriteSuccessCount()); } @@ -525,25 +440,27 @@ private void testServerPrematureDisconnect(TransferKind transferKind) throws IOE private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { // Setting a low transfer speed ensures that stream discarding will time out. - MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS); - transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); - server.enqueue(response); - server.enqueue(new MockResponse().setBody("Request #2")); - - URLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("ABCDE", readAscii(connection, 5)); + MockResponse mockResponse = new MockResponse() + .throttleBody(6, 1, TimeUnit.SECONDS); + transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); + server.enqueue(mockResponse); + server.enqueue(new MockResponse() + .setBody("Request #2")); + + Response response1 = get(server.getUrl("/")); + BufferedSource in = response1.body().source(); + assertEquals("ABCDE", in.readUtf8(5)); in.close(); try { - in.read(); - fail("Expected an IOException because the stream is closed."); - } catch (IOException expected) { + in.readByte(); + fail("Expected an IllegalStateException because the source is closed."); + } catch (IllegalStateException expected) { } assertEquals(1, cache.getWriteAbortCount()); assertEquals(0, cache.getWriteSuccessCount()); - connection = client.open(server.getUrl("/")); - assertEquals("Request #2", readAscii(connection)); + Response response2 = get(server.getUrl("/")); + assertEquals("Request #2", response2.body().string()); assertEquals(1, cache.getWriteAbortCount()); assertEquals(1, cache.getWriteSuccessCount()); } @@ -553,16 +470,18 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE // 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.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) + .setBody("A")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - URLConnection connection = client.open(url); - assertEquals("A", readAscii(connection)); - assertNull(connection.getHeaderField("Warning")); + Response response1 = get(url); + assertEquals("A", response1.body().string()); + + Response response2 = get(url); + assertEquals("A", response2.body().string()); + assertNull(response2.header("Warning")); } @Test public void defaultExpirationDateConditionallyCached() throws Exception { @@ -571,9 +490,9 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE // 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))); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } @@ -583,34 +502,34 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE // 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)) + server.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) .setBody("A")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection)); - assertEquals("113 HttpURLConnection \"Heuristic expiration\"", - connection.getHeaderField("Warning")); + assertEquals("A", get(server.getUrl("/")).body().string()); + Response response = get(server.getUrl("/")); + assertEquals("A", response.body().string()); + assertEquals("113 HttpURLConnection \"Heuristic expiration\"", response.header("Warning")); } @Test public void noDefaultExpirationForUrlsWithQueryString() 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() + .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) + .setBody("A")); server.enqueue(new MockResponse().setBody("B")); URL url = server.getUrl("/?foo=bar"); - assertEquals("A", readAscii(client.open(url))); - assertEquals("B", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); + assertEquals("B", get(url).body().string()); } @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached( - new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } @@ -631,10 +550,10 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() 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")); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Cache-Control: max-age=60")); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } @@ -647,12 +566,14 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE } @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { - assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + assertFullyCached(new MockResponse() + .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=60")); } @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { - assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60")); + assertFullyCached(new MockResponse() + .addHeader("Cache-Control: max-age=60")); } @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { @@ -713,25 +634,35 @@ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOE 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)) + 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.enqueue(new MockResponse() + .addHeader("X-Response-ID: 2")); URL url = server.getUrl("/"); - HttpURLConnection request1 = client.open(url); - request1.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, request1); - assertEquals("1", request1.getHeaderField("X-Response-ID")); + Request request = new Request.Builder() + .url(url) + .method(requestMethod, requestBodyOrNull(requestMethod)) + .build(); + Response response1 = client.newCall(request).execute(); + assertEquals("1", response1.header("X-Response-ID")); - URLConnection request2 = client.open(url); + Response response2 = get(url); if (expectCached) { - assertEquals("1", request2.getHeaderField("X-Response-ID")); + assertEquals("1", response2.header("X-Response-ID")); } else { - assertEquals("2", request2.getHeaderField("X-Response-ID")); + assertEquals("2", response2.header("X-Response-ID")); } } + private RequestBody requestBodyOrNull(String requestMethod) { + return (requestMethod.equals("POST") || requestMethod.equals("PUT")) + ? RequestBody.create(MediaType.parse("text/plain"), "foo") + : null; + } + @Test public void postInvalidatesCache() throws Exception { testMethodInvalidates("POST"); } @@ -748,21 +679,26 @@ 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.enqueue(new MockResponse() + .setBody("A") + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + server.enqueue(new MockResponse() + .setBody("B")); + server.enqueue(new MockResponse() + .setBody("C")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); - HttpURLConnection invalidate = client.open(url); - invalidate.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, invalidate); - assertEquals("B", readAscii(invalidate)); + Request request = new Request.Builder() + .url(url) + .method(requestMethod, requestBodyOrNull(requestMethod)) + .build(); + Response invalidate = client.newCall(request).execute(); + assertEquals("B", invalidate.body().string()); - assertEquals("C", readAscii(client.open(url))); + assertEquals("C", get(url).body().string()); } @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception { @@ -771,33 +707,38 @@ private void testMethodInvalidates(String requestMethod) throws Exception { // 3. expect a cache miss server.enqueue( new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B").setResponseCode(500)); - server.enqueue(new MockResponse().setBody("C")); + server.enqueue(new MockResponse() + .setBody("B") + .setResponseCode(500)); + server.enqueue(new MockResponse() + .setBody("C")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); - HttpURLConnection invalidate = client.open(url); - invalidate.setRequestMethod("POST"); - addRequestBodyIfNecessary("POST", invalidate); - assertEquals("B", readAscii(invalidate)); + Request request = new Request.Builder() + .url(url) + .method("POST", requestBodyOrNull("POST")) + .build(); + Response invalidate = client.newCall(request).execute(); + assertEquals("B", invalidate.body().string()); - assertEquals("C", readAscii(client.open(url))); + assertEquals("C", get(url).body().string()); } @Test public void etag() throws Exception { - RecordedRequest conditionalRequest = - assertConditionallyCached(new MockResponse().addHeader("ETag: v1")); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("ETag: v1")); assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); } @Test public void etagAndExpirationDateInThePast() 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))); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("ETag: v1") + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-None-Match: v1")); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); @@ -815,10 +756,10 @@ private void testMethodInvalidates(String requestMethod) throws Exception { @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() 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")); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Cache-Control: no-cache")); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } @@ -829,10 +770,10 @@ private void testMethodInvalidates(String requestMethod) throws Exception { @Test public void pragmaNoCacheAndExpirationDateInTheFuture() 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")); + RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Pragma: no-cache")); List headers = conditionalRequest.getHeaders(); assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); } @@ -850,7 +791,8 @@ private void testMethodInvalidates(String requestMethod) throws Exception { @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception { // 1. request a range // 2. request a full document, expecting a cache miss - server.enqueue(new MockResponse().setBody("AA") + server.enqueue(new MockResponse() + .setBody("AA") .setResponseCode(HttpURLConnection.HTTP_PARTIAL) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Content-Range: bytes 1000-1001/2000")); @@ -858,24 +800,29 @@ private void testMethodInvalidates(String requestMethod) throws Exception { URL url = server.getUrl("/"); - URLConnection range = client.open(url); - range.addRequestProperty("Range", "bytes=1000-1001"); - assertEquals("AA", readAscii(range)); + Request request = new Request.Builder() + .url(url) + .header("Range", "bytes=1000-1001") + .build(); + Response range = client.newCall(request).execute(); + assertEquals("AA", range.body().string()); - assertEquals("BB", readAscii(client.open(url))); + assertEquals("BB", get(url).body().string()); } @Test public void serverReturnsDocumentOlderThanCache() throws Exception { - server.enqueue(new MockResponse().setBody("A") + 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") + server.enqueue(new MockResponse() + .setBody("B") .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); + assertEquals("A", get(url).body().string()); } @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { @@ -891,17 +838,20 @@ private void testMethodInvalidates(String requestMethod) throws Exception { } private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { - server.enqueue( - response.setBody(gzip("ABCABCABC")).addHeader("Content-Encoding: gzip")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(response + .setBody(gzip("ABCABCABC")) + .addHeader("Content-Encoding: gzip")); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); // At least three request/response pairs are required because after the first request is cached // a different execution path might be taken. Thus modifications to the cache applied during // the second request might not be visible until another request is performed. - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); } @Test public void notModifiedSpecifiesEncoding() throws Exception { @@ -910,15 +860,13 @@ private void assertNonIdentityEncodingCached(MockResponse response) throws Excep .addHeader("Content-Encoding: gzip") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED) + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("Content-Encoding: gzip")); - server.enqueue(new MockResponse() - .setBody("DEFDEFDEF")); + server.enqueue(new MockResponse().setBody("DEFDEFDEF")); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); - assertEquals("DEFDEFDEF", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); + assertEquals("DEFDEFDEF", get(server.getUrl("/")).body().string()); } /** https://github.com/square/okhttp/issues/947 */ @@ -930,23 +878,24 @@ private void assertNonIdentityEncodingCached(MockResponse response) throws Excep .addHeader("Cache-Control: max-age=60")); server.enqueue(new MockResponse().setBody("FAIL")); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); - assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); + assertEquals("ABCABCABC", get(server.getUrl("/")).body().string()); } @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception { - server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A")); + server.enqueue(new MockResponse() + .addHeader("ETag: v1").setBody("A")); server.enqueue(new MockResponse() .clearHeaders() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); ConnectionPool pool = ConnectionPool.getDefault(); pool.evictAll(); - client.client().setConnectionPool(pool); + client.setConnectionPool(pool); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals(1, client.client().getConnectionPool().getConnectionCount()); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals(1, client.getConnectionPool().getConnectionCount()); } @Test public void expiresDateBeforeModifiedDate() throws Exception { @@ -956,144 +905,187 @@ private void assertNonIdentityEncodingCached(MockResponse response) throws Excep } @Test public void requestMaxAge() throws IOException { - server.enqueue(new MockResponse().setBody("A") + 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")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-age=30"); - assertEquals("B", readAscii(connection)); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "max-age=30") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void requestMinFresh() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=60") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); server.enqueue(new MockResponse().setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "min-fresh=120"); - assertEquals("B", readAscii(connection)); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "min-fresh=120") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void requestMaxStale() throws IOException { - server.enqueue(new MockResponse().setBody("A") + 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.enqueue(new MockResponse() + .setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("A", readAscii(connection)); - assertEquals("110 HttpURLConnection \"Response is stale\"", - connection.getHeaderField("Warning")); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "max-stale=180") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("A", response.body().string()); + assertEquals("110 HttpURLConnection \"Response is stale\"", response.header("Warning")); } @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException { - server.enqueue(new MockResponse().setBody("A") + 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")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("B", readAscii(connection)); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "max-stale=180") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { // (no responses enqueued) - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "only-if-cached") + .build(); + Response response = client.newCall(request).execute(); + assertTrue(response.body().source().exhausted()); + assertEquals(504, response.code()); assertEquals(1, cache.getRequestCount()); assertEquals(0, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); } @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertEquals("A", readAscii(connection)); + assertEquals("A", get(server.getUrl("/")).body().string()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "only-if-cached") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("A", response.body().string()); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(1, cache.getHitCount()); } @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); + assertEquals("A", get(server.getUrl("/")).body().string()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "only-if-cached") + .build(); + Response response = client.newCall(request).execute(); + assertTrue(response.body().source().exhausted()); + assertEquals(504, response.code()); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); } @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A")); + server.enqueue(new MockResponse() + .setBody("A")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); + assertEquals("A", get(server.getUrl("/")).body().string()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "only-if-cached") + .build(); + Response response = client.newCall(request).execute(); + assertTrue(response.body().source().exhausted()); + assertEquals(504, response.code()); assertEquals(2, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); } @Test public void requestCacheControlNoCache() 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.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")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - URLConnection connection = client.open(url); - connection.setRequestProperty("Cache-Control", "no-cache"); - assertEquals("B", readAscii(connection)); + assertEquals("A", get(url).body().string()); + Request request = new Request.Builder() + .url(url) + .header("Cache-Control", "no-cache") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void requestPragmaNoCache() 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.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")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - URLConnection connection = client.open(url); - connection.setRequestProperty("Pragma", "no-cache"); - assertEquals("B", readAscii(connection)); + assertEquals("A", get(url).body().string()); + Request request = new Request.Builder() + .url(url) + .header("Pragma", "no-cache") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception { - MockResponse response = - new MockResponse().addHeader("ETag: v3").addHeader("Cache-Control: max-age=0"); + 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); @@ -1104,7 +1096,8 @@ private void assertNonIdentityEncodingCached(MockResponse response) throws Excep @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); - MockResponse response = new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + 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"); @@ -1116,38 +1109,24 @@ private void assertNonIdentityEncodingCached(MockResponse response) throws Excep 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.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); - HttpURLConnection connection = client.open(url); - connection.addRequestProperty(conditionName, conditionValue); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); + Request request = new Request.Builder() + .url(url) + .header(conditionName, conditionValue) + .build(); + Response response = client.newCall(request).execute(); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, response.code()); + assertEquals("", response.body().string()); server.takeRequest(); // seed return server.takeRequest(); } - /** - * Confirm that {@link URLConnection#setIfModifiedSince} causes an - * If-Modified-Since header with a GMT timestamp. - * - * https://code.google.com/p/android/issues/detail?id=66135 - */ - @Test public void setIfModifiedSince() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - - URL url = server.getUrl("/"); - URLConnection connection = client.open(url); - connection.setIfModifiedSince(1393666200000L); - assertEquals("A", readAscii(connection)); - RecordedRequest request = server.takeRequest(); - String ifModifiedSinceHeader = request.getHeader("If-Modified-Since"); - assertEquals("Sat, 01 Mar 2014 09:30:00 GMT", ifModifiedSinceHeader); - } - /** * For Last-Modified and Date headers, we should echo the date back in the * exact format we were served. @@ -1169,8 +1148,8 @@ private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("A", get(server.getUrl("/")).body().string()); // The first request has no conditions. RecordedRequest request1 = server.takeRequest(); @@ -1182,27 +1161,34 @@ private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String } @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - HttpURLConnection connection = client.open(server.getUrl("/")); - String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); - connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("If-Modified-Since", formatDate(-24, TimeUnit.HOURS)) + .build(); + Response response = client.newCall(request).execute(); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, response.code()); + assertEquals("", response.body().string()); } @Test public void authorizationRequestHeaderPreventsCaching() 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.enqueue(new MockResponse() + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: max-age=60") + .setBody("A")); + server.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection = client.open(url); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(client.open(url))); + Request request = new Request.Builder() + .url(url) + .header("Authorization", "password") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("A", response.body().string()); + assertEquals("B", get(url).body().string()); } @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { @@ -1219,75 +1205,48 @@ private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String 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")); + public void assertAuthorizationRequestFullyCached(MockResponse mockResponse) throws Exception { + server.enqueue(mockResponse + .addHeader("Cache-Control: max-age=60") + .setBody("A")); + server.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection = client.open(url); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("A", readAscii(client.open(url))); + Request request = new Request.Builder() + .url(url) + .header("Authorization", "password") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("A", response.body().string()); + assertEquals("A", get(url).body().string()); } @Test public void contentLocationDoesNotPopulateCache() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Content-Location: /bar") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - - assertEquals("A", readAscii(client.open(server.getUrl("/foo")))); - assertEquals("B", readAscii(client.open(server.getUrl("/bar")))); - } - - @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { - server.enqueue( - new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - - URLConnection connection = client.open(server.getUrl("/")); - connection.setUseCaches(false); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(client.open(server.getUrl("/")))); - } - - @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { - server.enqueue( - new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - URLConnection connection = client.open(server.getUrl("/")); - connection.setUseCaches(false); - assertEquals("B", readAscii(connection)); - } + server.enqueue(new MockResponse() + .setBody("B")); - @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { - URL url = new URL("http://localhost/"); - URLConnection c1 = client.open(url); - URLConnection c2 = client.open(url); - assertTrue(c1.getDefaultUseCaches()); - c1.setDefaultUseCaches(false); - try { - assertTrue(c1.getUseCaches()); - assertTrue(c2.getUseCaches()); - URLConnection c3 = client.open(url); - assertFalse(c3.getUseCaches()); - } finally { - c1.setDefaultUseCaches(true); - } + assertEquals("A", get(server.getUrl("/foo")).body().string()); + assertEquals("B", get(server.getUrl("/bar")).body().string()); } @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + 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.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse() + .setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/a")))); - assertEquals("A", readAscii(client.open(server.getUrl("/a")))); - assertEquals("B", readAscii(client.open(server.getUrl("/b")))); + assertEquals("A", get(server.getUrl("/a")).body().string()); + assertEquals("A", get(server.getUrl("/a")).body().string()); + assertEquals("B", get(server.getUrl("/b")).body().string()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); @@ -1295,191 +1254,257 @@ public void assertAuthorizationRequestFullyCached(MockResponse response) throws } @Test public void statisticsConditionalCacheMiss() throws Exception { - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + 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.enqueue(new MockResponse() + .setBody("B")); + server.enqueue(new MockResponse() + .setBody("C")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); - assertEquals("B", readAscii(client.open(server.getUrl("/")))); - assertEquals("C", readAscii(client.open(server.getUrl("/")))); + assertEquals("B", get(server.getUrl("/")).body().string()); + assertEquals("C", get(server.getUrl("/")).body().string()); assertEquals(3, cache.getRequestCount()); assertEquals(3, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); } @Test public void statisticsConditionalCacheHit() throws Exception { - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + 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.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("A", get(server.getUrl("/")).body().string()); assertEquals(3, cache.getRequestCount()); assertEquals(3, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } @Test public void statisticsFullCacheHit() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A")); + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60").setBody("A")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("A", get(server.getUrl("/")).body().string()); assertEquals(3, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } @Test public void varyMatchesChangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - HttpURLConnection frConnection = client.open(url); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(frConnection)); + Request frRequest = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .build(); + Response frResponse = client.newCall(frRequest).execute(); + assertEquals("A", frResponse.body().string()); - HttpURLConnection enConnection = client.open(url); - enConnection.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(enConnection)); + Request enRequest = new Request.Builder() + .url(url) + .header("Accept-Language", "en-US") + .build(); + Response enResponse = client.newCall(enRequest).execute(); + assertEquals("B", enResponse.body().string()); } @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection1 = client.open(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = client.open(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection2)); + Request request = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .build(); + Response response1 = client.newCall(request).execute(); + assertEquals("A", response1.body().string()); + Request request1 = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .build(); + Response response2 = client.newCall(request1).execute(); + assertEquals("A", response2.body().string()); } @Test public void varyMatchesAbsentRequestHeaderField() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("A", get(server.getUrl("/")).body().string()); } @Test public void varyMatchesAddedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - URLConnection fooConnection = client.open(server.getUrl("/")); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("B", readAscii(fooConnection)); + assertEquals("A", get(server.getUrl("/")).body().string()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Foo", "bar") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("B", response.body().string()); } @Test public void varyMatchesRemovedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); - URLConnection fooConnection = client.open(server.getUrl("/")); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("A", readAscii(fooConnection)); - assertEquals("B", readAscii(client.open(server.getUrl("/")))); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Foo", "bar") + .build(); + Response fooresponse = client.newCall(request).execute(); + assertEquals("A", fooresponse.body().string()); + assertEquals("B", get(server.getUrl("/")).body().string()); } @Test public void varyFieldsAreCaseInsensitive() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: ACCEPT-LANGUAGE") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection1 = client.open(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = client.open(url); - connection2.addRequestProperty("accept-language", "fr-CA"); - assertEquals("A", readAscii(connection2)); + Request request = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .build(); + Response response1 = client.newCall(request).execute(); + assertEquals("A", response1.body().string()); + Request request1 = new Request.Builder() + .url(url) + .header("accept-language", "fr-CA") + .build(); + Response response2 = client.newCall(request1).execute(); + assertEquals("A", response2.body().string()); } @Test public void varyMultipleFieldsWithMatch() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + 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.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection1 = client.open(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - connection1.addRequestProperty("Accept-Charset", "UTF-8"); - connection1.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = client.open(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Charset", "UTF-8"); - connection2.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection2)); + Request request = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .header("Accept-Charset", "UTF-8") + .header("Accept-Encoding", "identity") + .build(); + Response response1 = client.newCall(request).execute(); + assertEquals("A", response1.body().string()); + Request request1 = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .header("Accept-Charset", "UTF-8") + .header("Accept-Encoding", "identity") + .build(); + Response response2 = client.newCall(request1).execute(); + assertEquals("A", response2.body().string()); } @Test public void varyMultipleFieldsWithNoMatch() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + 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.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection frConnection = client.open(url); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - frConnection.addRequestProperty("Accept-Charset", "UTF-8"); - frConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(frConnection)); - URLConnection enConnection = client.open(url); - enConnection.addRequestProperty("Accept-Language", "en-CA"); - enConnection.addRequestProperty("Accept-Charset", "UTF-8"); - enConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("B", readAscii(enConnection)); + Request frRequest = new Request.Builder() + .url(url) + .header("Accept-Language", "fr-CA") + .header("Accept-Charset", "UTF-8") + .header("Accept-Encoding", "identity") + .build(); + Response frResponse = client.newCall(frRequest).execute(); + assertEquals("A", frResponse.body().string()); + Request enRequest = new Request.Builder() + .url(url) + .header("Accept-Language", "en-CA") + .header("Accept-Charset", "UTF-8") + .header("Accept-Encoding", "identity") + .build(); + Response enResponse = client.newCall(enRequest).execute(); + assertEquals("B", enResponse.body().string()); } @Test public void varyMultipleFieldValuesWithMatch() 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.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - URLConnection connection1 = client.open(url); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = client.open(url); - connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); + Request request1 = new Request.Builder() + .url(url) + .addHeader("Accept-Language", "fr-CA, fr-FR") + .addHeader("Accept-Language", "en-US") + .build(); + Response response1 = client.newCall(request1).execute(); + assertEquals("A", response1.body().string()); + + Request request2 = new Request.Builder() + .url(url) + .addHeader("Accept-Language", "fr-CA, fr-FR") + .addHeader("Accept-Language", "en-US") + .build(); + Response response2 = client.newCall(request2).execute(); + assertEquals("A", response2.body().string()); } @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception { @@ -1489,129 +1514,149 @@ public void assertAuthorizationRequestFullyCached(MockResponse response) throws server.enqueue(new MockResponse().setBody("B")); URL url = server.getUrl("/"); - URLConnection connection1 = client.open(url); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = client.open(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(connection2)); + Request request1 = new Request.Builder() + .url(url) + .addHeader("Accept-Language", "fr-CA, fr-FR") + .addHeader("Accept-Language", "en-US") + .build(); + Response response1 = client.newCall(request1).execute(); + assertEquals("A", response1.body().string()); + + Request request2 = new Request.Builder() + .url(url) + .addHeader("Accept-Language", "fr-CA") + .addHeader("Accept-Language", "en-US") + .build(); + Response response2 = client.newCall(request2).execute(); + assertEquals("B", response2.body().string()); } @Test public void varyAsterisk() throws Exception { - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") - .addHeader("Vary: *") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue( + new MockResponse().addHeader("Cache-Control: max-age=60").addHeader("Vary: *").setBody("A")); + server.enqueue(new MockResponse() + .setBody("B")); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - assertEquals("B", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", get(server.getUrl("/")).body().string()); + assertEquals("B", get(server.getUrl("/")).body().string()); } @Test public void varyAndHttps() throws Exception { server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse() + .setBody("B")); + + client.setSslSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); URL url = server.getUrl("/"); - HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url); - connection1.setSSLSocketFactory(sslContext.getSocketFactory()); - connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); + Request request1 = new Request.Builder() + .url(url) + .header("Accept-Language", "en-US") + .build(); + Response response1 = client.newCall(request1).execute(); + assertEquals("A", response1.body().string()); - HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url); - connection2.setSSLSocketFactory(sslContext.getSocketFactory()); - connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); + Request request2 = new Request.Builder() + .url(url) + .header("Accept-Language", "en-US") + .build(); + Response response2 = client.newCall(request2).execute(); + assertEquals("A", response2.body().string()); } @Test public void cachePlusCookies() throws Exception { - server.enqueue(new MockResponse().addHeader( - "Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") + 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() + ";") + server.enqueue(new MockResponse() + .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); assertCookies(url, "a=FIRST"); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); assertCookies(url, "a=SECOND"); } @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception { - server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD") + 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") + server.enqueue(new MockResponse() + .addHeader("Allow: GET, HEAD, PUT") .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - URLConnection connection1 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); + Response response1 = get(server.getUrl("/")); + assertEquals("A", response1.body().string()); + assertEquals("GET, HEAD", response1.header("Allow")); - URLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); + Response response2 = get(server.getUrl("/")); + assertEquals("A", response2.body().string()); + assertEquals("GET, HEAD, PUT", response2.header("Allow")); } @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception { - server.enqueue(new MockResponse().addHeader("Transfer-Encoding: identity") + 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)); - URLConnection connection1 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); + Response response1 = get(server.getUrl("/")); + assertEquals("A", response1.body().string()); + assertEquals("identity", response1.header("Transfer-Encoding")); - URLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); + Response response2 = get(server.getUrl("/")); + assertEquals("A", response2.body().string()); + assertEquals("identity", response2.header("Transfer-Encoding")); } @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception { - server.enqueue(new MockResponse().addHeader("Warning: 199 test danger") + 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.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - URLConnection connection1 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("199 test danger", connection1.getHeaderField("Warning")); + Response response1 = get(server.getUrl("/")); + assertEquals("A", response1.body().string()); + assertEquals("199 test danger", response1.header("Warning")); - URLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals(null, connection2.getHeaderField("Warning")); + Response response2 = get(server.getUrl("/")); + assertEquals("A", response2.body().string()); + assertEquals(null, response2.header("Warning")); } @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception { - server.enqueue(new MockResponse().addHeader("Warning: 299 test danger") + 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.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - URLConnection connection1 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("299 test danger", connection1.getHeaderField("Warning")); + Response response1 = get(server.getUrl("/")); + assertEquals("A", response1.body().string()); + assertEquals("299 test danger", response1.header("Warning")); - URLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("299 test danger", connection2.getHeaderField("Warning")); + Response response2 = get(server.getUrl("/")); + assertEquals("A", response2.body().string()); + assertEquals("299 test danger", response2.header("Warning")); } public void assertCookies(URL url, String... expectedCookies) throws Exception { @@ -1630,73 +1675,85 @@ public void assertCookies(URL url, String... expectedCookies) throws Exception { } @Test public void conditionalHitUpdatesCache() throws Exception { - server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) + 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") + 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.enqueue(new MockResponse() + .setBody("B")); // cache miss; seed the cache - HttpURLConnection connection1 = client.open(server.getUrl("/a")); - assertEquals("A", readAscii(connection1)); - assertEquals(null, connection1.getHeaderField("Allow")); + Response response1 = get(server.getUrl("/a")); + assertEquals("A", response1.body().string()); + assertEquals(null, response1.header("Allow")); // conditional cache hit; update the cache - HttpURLConnection connection2 = client.open(server.getUrl("/a")); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); + Response response2 = get(server.getUrl("/a")); + assertEquals(HttpURLConnection.HTTP_OK, response2.code()); + assertEquals("A", response2.body().string()); + assertEquals("GET, HEAD", response2.header("Allow")); // full cache hit - HttpURLConnection connection3 = client.open(server.getUrl("/a")); - assertEquals("A", readAscii(connection3)); - assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); + Response response3 = get(server.getUrl("/a")); + assertEquals("A", response3.body().string()); + assertEquals("GET, HEAD", response3.header("Allow")); assertEquals(2, server.getRequestCount()); } @Test public void responseSourceHeaderCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertEquals("A", readAscii(connection)); + assertEquals("A", get(server.getUrl("/")).body().string()); + Request request = new Request.Builder() + .url(server.getUrl("/")) + .header("Cache-Control", "only-if-cached") + .build(); + Response response = client.newCall(request).execute(); + assertEquals("A", response.body().string()); } @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B") + server.enqueue(new MockResponse() + .setBody("B") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("B", readAscii(connection)); + assertEquals("A", get(server.getUrl("/")).body().string()); + Response response = get(server.getUrl("/")); + assertEquals("B", response.body().string()); } @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException { - server.enqueue(new MockResponse().setBody("A") + server.enqueue(new MockResponse() + .setBody("A") .addHeader("Cache-Control: max-age=0") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setResponseCode(304)); + server.enqueue(new MockResponse() + .setResponseCode(304)); - assertEquals("A", readAscii(client.open(server.getUrl("/")))); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection)); + assertEquals("A", get(server.getUrl("/")).body().string()); + Response response = get(server.getUrl("/")); + assertEquals("A", response.body().string()); } @Test public void responseSourceHeaderFetched() throws IOException { - server.enqueue(new MockResponse().setBody("A")); + server.enqueue(new MockResponse() + .setBody("A")); - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection)); + Response response = get(server.getUrl("/")); + assertEquals("A", response.body().string()); } @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception { @@ -1705,8 +1762,8 @@ public void assertCookies(URL url, String... expectedCookies) throws Exception { .addHeader(": A") .setBody("body")); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", connection.getHeaderField("")); + Response response = get(server.getUrl("/")); + assertEquals("A", response.header("")); } /** @@ -1761,12 +1818,19 @@ public void assertCookies(URL url, String... expectedCookies) throws Exception { writeFile(cache.getDirectory(), urlKey + ".1", entryBody); writeFile(cache.getDirectory(), "journal", journalBody); cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE); - client.client().setCache(cache); + client.setCache(cache); + + Response response = get(url); + assertEquals(entryBody, response.body().string()); + assertEquals("3", response.header("Content-Length")); + assertEquals("foo", response.header("etag")); + } - HttpURLConnection connection = client.open(url); - assertEquals(entryBody, readAscii(connection)); - assertEquals("3", connection.getHeaderField("Content-Length")); - assertEquals("foo", connection.getHeaderField("etag")); + private Response get(URL url) throws IOException { + Request request = new Request.Builder() + .url(url) + .build(); + return client.newCall(request).execute(); } private void writeFile(File directory, String file, String content) throws IOException { @@ -1790,54 +1854,47 @@ private String formatDate(Date date) { 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.enqueue(new MockResponse() + .setBody("B")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - assertEquals("B", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); + assertEquals("B", get(url).body().string()); } /** @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)); + 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.enqueue(new MockResponse() + .setStatus("HTTP/1.1 200 C-OK").setBody("C")); URL valid = server.getUrl("/valid"); - HttpURLConnection connection1 = client.open(valid); - assertEquals("A", readAscii(connection1)); - assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); - assertEquals("A-OK", connection1.getResponseMessage()); - HttpURLConnection connection2 = client.open(valid); - assertEquals("A", readAscii(connection2)); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A-OK", connection2.getResponseMessage()); + Response response1 = get(valid); + assertEquals("A", response1.body().string()); + assertEquals(HttpURLConnection.HTTP_OK, response1.code()); + assertEquals("A-OK", response1.message()); + Response response2 = get(valid); + assertEquals("A", response2.body().string()); + assertEquals(HttpURLConnection.HTTP_OK, response2.code()); + assertEquals("A-OK", response2.message()); URL invalid = server.getUrl("/invalid"); - HttpURLConnection connection3 = client.open(invalid); - assertEquals("B", readAscii(connection3)); - assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); - assertEquals("B-OK", connection3.getResponseMessage()); - HttpURLConnection connection4 = client.open(invalid); - assertEquals("C", readAscii(connection4)); - assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); - assertEquals("C-OK", connection4.getResponseMessage()); + Response response3 = get(invalid); + assertEquals("B", response3.body().string()); + assertEquals(HttpURLConnection.HTTP_OK, response3.code()); + assertEquals("B-OK", response3.message()); + Response response4 = get(invalid); + assertEquals("C", response4.body().string()); + assertEquals(HttpURLConnection.HTTP_OK, response4.code()); + assertEquals("C-OK", response4.message()); server.takeRequest(); // regular get return server.takeRequest(); // conditional get @@ -1848,8 +1905,8 @@ private void assertFullyCached(MockResponse response) throws Exception { server.enqueue(response.setBody("B")); URL url = server.getUrl("/"); - assertEquals("A", readAscii(client.open(url))); - assertEquals("A", readAscii(client.open(url))); + assertEquals("A", get(url).body().string()); + assertEquals("A", get(url).body().string()); } /** @@ -1868,48 +1925,6 @@ private MockResponse truncateViolently(MockResponse response, int numBytesToKeep 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, Buffer content, int chunkSize) @@ -1942,10 +1957,6 @@ void setBody(MockResponse response, String content, int chunkSize) throws IOExce } } - private List toListOrNull(T[] arrayOrNull) { - return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; - } - /** Returns a gzipped copy of {@code bytes}. */ public Buffer gzip(String data) throws IOException { Buffer result = new Buffer(); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java index 252365264f3f..0bb8d1a80d8c 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java @@ -21,11 +21,17 @@ import com.squareup.okhttp.internal.tls.OkHostnameVerifier; import java.io.IOException; import java.net.Authenticator; +import java.net.CacheRequest; +import java.net.CacheResponse; import java.net.CookieHandler; import java.net.CookieManager; import java.net.ProxySelector; import java.net.ResponseCache; +import java.net.URI; +import java.net.URLConnection; import java.util.Arrays; +import java.util.List; +import java.util.Map; import javax.net.SocketFactory; import org.junit.After; import org.junit.Test; @@ -95,9 +101,17 @@ public final class OkHttpClientTest { assertNull(client.getCache()); } - @Test public void copyWithDefaultsDoesNotHonorGlobalResponseCache() throws Exception { - ResponseCache responseCache = new AbstractResponseCache(); - ResponseCache.setDefault(responseCache); + @Test public void copyWithDefaultsDoesNotHonorGlobalResponseCache() { + ResponseCache.setDefault(new ResponseCache() { + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + throw new AssertionError(); + } + + @Override public CacheRequest put(URI uri, URLConnection connection) { + throw new AssertionError(); + } + }); OkHttpClient client = new OkHttpClient().copyWithDefaults(); assertNull(client.internalCache()); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java index 9b29ba3f7785..3671a7e76e64 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java @@ -16,7 +16,6 @@ package com.squareup.okhttp.internal.http; -import com.squareup.okhttp.AbstractResponseCache; import com.squareup.okhttp.Cache; import com.squareup.okhttp.Challenge; import com.squareup.okhttp.ConnectionConfiguration; @@ -33,7 +32,6 @@ import com.squareup.okhttp.internal.SingleInetAddressNetwork; import com.squareup.okhttp.internal.SslContextBuilder; import com.squareup.okhttp.internal.Util; -import com.squareup.okhttp.internal.huc.CacheAdapter; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; @@ -43,7 +41,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.Authenticator; -import java.net.CacheRequest; import java.net.ConnectException; import java.net.HttpRetryException; import java.net.HttpURLConnection; @@ -70,7 +67,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPInputStream; import javax.net.SocketFactory; import javax.net.ssl.HttpsURLConnection; @@ -2004,8 +2000,7 @@ private void testResponseRedirectedWithPost(int redirectCode, TransferKind trans } @Test public void redirectedPostStripsRequestBodyHeaders() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /page2")); server.enqueue(new MockResponse().setBody("Page 2")); server.play(); @@ -2430,33 +2425,6 @@ private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws I } } - /** Don't explode if the cache returns a null body. http://b/3373699 */ - @Test public void responseCacheReturnsNullOutputStream() throws Exception { - final AtomicBoolean aborted = new AtomicBoolean(); - Internal.instance.setCache(client.client(), new CacheAdapter(new AbstractResponseCache() { - @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { - return new CacheRequest() { - @Override public void abort() { - aborted.set(true); - } - - @Override public OutputStream getBody() throws IOException { - return null; - } - }; - } - })); - - server.enqueue(new MockResponse().setBody("abcdef")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("abc", readAscii(in, 3)); - in.close(); - assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here - } - /** http://code.google.com/p/android/issues/detail?id=14562 */ @Test public void readAfterLastByte() throws Exception { server.enqueue(new MockResponse().setBody("ABC") diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AbstractResponseCache.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/AbstractResponseCache.java similarity index 100% rename from okhttp-tests/src/test/java/com/squareup/okhttp/AbstractResponseCache.java rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/AbstractResponseCache.java diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java new file mode 100644 index 000000000000..2d6de3ebb884 --- /dev/null +++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java @@ -0,0 +1,1864 @@ +/* + * 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 com.squareup.okhttp; + +import com.squareup.okhttp.internal.SslContextBuilder; +import com.squareup.okhttp.internal.Util; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; +import java.io.BufferedReader; +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.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.ResponseCache; +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.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import okio.Buffer; +import okio.BufferedSink; +import okio.GzipSink; +import okio.Okio; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** Test caching with {@link OkUrlFactory}. */ +public final class UrlConnectionCacheTest { + private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + + private static final SSLContext sslContext = SslContextBuilder.localhost(); + + @Rule public TemporaryFolder cacheRule = new TemporaryFolder(); + @Rule public MockWebServerRule serverRule = new MockWebServerRule(); + @Rule public MockWebServerRule server2Rule = new MockWebServerRule(); + + private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient()); + private MockWebServer server; + private MockWebServer server2; + private Cache cache; + private final CookieManager cookieManager = new CookieManager(); + + @Before public void setUp() throws Exception { + server = serverRule.get(); + server.setProtocolNegotiationEnabled(false); + server2 = server2Rule.get(); + cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE); + client.client().setCache(cache); + CookieHandler.setDefault(cookieManager); + } + + @After public void tearDown() throws Exception { + ResponseCache.setDefault(null); + CookieHandler.setDefault(null); + } + + @Test public void responseCacheAccessWithOkHttpMember() throws IOException { + assertSame(cache, client.client().getCache()); + assertNull(client.getResponseCache()); + } + + /** + * Test that response caching is consistent with the RI and the spec. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 + */ + @Test public void responseCachingByResponseCode() 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 <= 307; ++i) { + assertCached(false, i); + } + assertCached(true, 308); + 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); + } + } + + @Test public void 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 = client.open(url); + assertEquals(responseCode, conn.getResponseCode()); + + // exhaust the content stream + readAscii(conn); + + Response cached = cache.get(new Request.Builder().url(url).build()); + if (shouldPut) { + assertNotNull(Integer.toString(responseCode), cached); + cached.body().close(); + } else { + assertNull(Integer.toString(responseCode), cached); + } + server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers + } + + @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { + testResponseCaching(TransferKind.FIXED_LENGTH); + } + + @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { + testResponseCaching(TransferKind.CHUNKED); + } + + @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() 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); + + // Make sure that calling skip() doesn't omit bytes from the cache. + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + 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 = client.open(server.getUrl("/")); // 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()); + } + + @Test public void secureResponseCaching() throws IOException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setBody("ABC")); + + HttpsURLConnection c1 = (HttpsURLConnection) client.open(server.getUrl("/")); + c1.setSSLSocketFactory(sslContext.getSocketFactory()); + c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(c1)); + + // OpenJDK 6 fails on this line, complaining that the connection isn't open yet + String suite = c1.getCipherSuite(); + List localCerts = toListOrNull(c1.getLocalCertificates()); + List serverCerts = toListOrNull(c1.getServerCertificates()); + Principal peerPrincipal = c1.getPeerPrincipal(); + Principal localPrincipal = c1.getLocalPrincipal(); + + HttpsURLConnection c2 = (HttpsURLConnection) client.open(server.getUrl("/")); // cached! + c2.setSSLSocketFactory(sslContext.getSocketFactory()); + c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(c2)); + + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(1, cache.getHitCount()); + + assertEquals(suite, c2.getCipherSuite()); + assertEquals(localCerts, toListOrNull(c2.getLocalCertificates())); + assertEquals(serverCerts, toListOrNull(c2.getServerCertificates())); + assertEquals(peerPrincipal, c2.getPeerPrincipal()); + assertEquals(localPrincipal, c2.getLocalPrincipal()); + } + + @Test public void responseCachingAndRedirects() 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")); + + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("ABC", readAscii(connection)); + + connection = client.open(server.getUrl("/")); // cached! + assertEquals("ABC", readAscii(connection)); + + assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects + assertEquals(2, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void redirectToCachedResult() 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")); + + assertEquals("ABC", readAscii(client.open(server.getUrl("/foo")))); + RecordedRequest request1 = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); + assertEquals(0, request1.getSequenceNumber()); + + assertEquals("ABC", readAscii(client.open(server.getUrl("/bar")))); + 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(client.open(server.getUrl("/baz")))); + RecordedRequest request3 = server.takeRequest(); + assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); + assertEquals(2, request3.getSequenceNumber()); + } + + @Test public void secureResponseCachingAndRedirects() throws IOException { + server.useHttps(sslContext.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")); + + client.client().setSslSocketFactory(sslContext.getSocketFactory()); + client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + + HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); + assertEquals("ABC", readAscii(connection1)); + assertNotNull(connection1.getCipherSuite()); + + // Cached! + HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); + assertEquals("ABC", readAscii(connection2)); + assertNotNull(connection2.getCipherSuite()); + + assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 + assertEquals(2, cache.getHitCount()); + assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite()); + } + + /** + * We've had bugs where caching and cross-protocol redirects yield class + * cast exceptions internal to the cache because we incorrectly assumed that + * HttpsURLConnection was always HTTPS and HttpURLConnection was always HTTP; + * in practice redirects mean that each can do either. + * + * https://github.com/square/okhttp/issues/214 + */ + @Test public void secureResponseCachingAndProtocolRedirects() throws IOException { + server2.useHttps(sslContext.getSocketFactory(), false); + server2.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setBody("ABC")); + server2.enqueue(new MockResponse().setBody("DEF")); + + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) + .addHeader("Location: " + server2.getUrl("/"))); + + client.client().setSslSocketFactory(sslContext.getSocketFactory()); + client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + + HttpURLConnection connection1 = client.open(server.getUrl("/")); + assertEquals("ABC", readAscii(connection1)); + + // Cached! + HttpURLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("ABC", readAscii(connection2)); + + assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 + assertEquals(2, cache.getHitCount()); + } + + @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { + testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); + } + + @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException { + testServerPrematureDisconnect(TransferKind.CHUNKED); + } + + @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() 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")); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.open(server.getUrl("/")).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 = client.open(server.getUrl("/")); + assertEquals("Request #2", readAscii(connection)); + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(1, cache.getWriteSuccessCount()); + } + + @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException { + testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); + } + + @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException { + testClientPrematureDisconnect(TransferKind.CHUNKED); + } + + @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException { + testClientPrematureDisconnect(TransferKind.END_OF_STREAM); + } + + private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { + // Setting a low transfer speed ensures that stream discarding will time out. + MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS); + transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("Request #2")); + + URLConnection connection = client.open(server.getUrl("/")); + 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 = client.open(server.getUrl("/")); + assertEquals("Request #2", readAscii(connection)); + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(1, cache.getWriteSuccessCount()); + } + + @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() 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")); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + URLConnection connection = client.open(url); + assertEquals("A", readAscii(connection)); + assertNull(connection.getHeaderField("Warning")); + } + + @Test public void defaultExpirationDateConditionallyCached() 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 headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection)); + assertEquals("113 HttpURLConnection \"Heuristic expiration\"", + connection.getHeaderField("Warning")); + } + + @Test public void noDefaultExpirationForUrlsWithQueryString() 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")); + + URL url = server.getUrl("/?foo=bar"); + assertEquals("A", readAscii(client.open(url))); + assertEquals("B", readAscii(client.open(url))); + } + + @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception { + assertNotCached(new MockResponse().addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + } + + @Test public void expirationDateInTheFuture() throws Exception { + assertFullyCached(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + } + + @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() 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 headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() 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")); + } + + @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { + assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60")); + } + + @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { + assertFullyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { + assertFullyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: s-maxage=60") + .addHeader("Cache-Control: max-age=180")); + } + + @Test public void maxAgePreferredOverHigherMaxAge() throws Exception { + assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: s-maxage=180") + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void requestMethodOptionsIsNotCached() throws Exception { + testRequestMethod("OPTIONS", false); + } + + @Test public void requestMethodGetIsCached() throws Exception { + testRequestMethod("GET", true); + } + + @Test public void requestMethodHeadIsNotCached() throws Exception { + // We could support this but choose not to for implementation simplicity + testRequestMethod("HEAD", false); + } + + @Test public void requestMethodPostIsNotCached() throws Exception { + // We could support this but choose not to for implementation simplicity + testRequestMethod("POST", false); + } + + @Test public void requestMethodPutIsNotCached() throws Exception { + testRequestMethod("PUT", false); + } + + @Test public void requestMethodDeleteIsNotCached() throws Exception { + testRequestMethod("DELETE", false); + } + + @Test public void requestMethodTraceIsNotCached() 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")); + + URL url = server.getUrl("/"); + + HttpURLConnection request1 = client.open(url); + request1.setRequestMethod(requestMethod); + addRequestBodyIfNecessary(requestMethod, request1); + assertEquals("1", request1.getHeaderField("X-Response-ID")); + + URLConnection request2 = client.open(url); + if (expectCached) { + assertEquals("1", request2.getHeaderField("X-Response-ID")); + } else { + assertEquals("2", request2.getHeaderField("X-Response-ID")); + } + } + + @Test public void postInvalidatesCache() throws Exception { + testMethodInvalidates("POST"); + } + + @Test public void putInvalidatesCache() throws Exception { + testMethodInvalidates("PUT"); + } + + @Test public void deleteMethodInvalidatesCache() 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")); + + URL url = server.getUrl("/"); + + assertEquals("A", readAscii(client.open(url))); + + HttpURLConnection invalidate = client.open(url); + invalidate.setRequestMethod(requestMethod); + addRequestBodyIfNecessary(requestMethod, invalidate); + assertEquals("B", readAscii(invalidate)); + + assertEquals("C", readAscii(client.open(url))); + } + + @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception { + // 1. seed the cache + // 2. invalidate it with uncacheable response + // 3. expect a cache miss + server.enqueue( + new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + server.enqueue(new MockResponse().setBody("B").setResponseCode(500)); + server.enqueue(new MockResponse().setBody("C")); + + URL url = server.getUrl("/"); + + assertEquals("A", readAscii(client.open(url))); + + HttpURLConnection invalidate = client.open(url); + invalidate.setRequestMethod("POST"); + addRequestBodyIfNecessary("POST", invalidate); + assertEquals("B", readAscii(invalidate)); + + assertEquals("C", readAscii(client.open(url))); + } + + @Test public void etag() throws Exception { + RecordedRequest conditionalRequest = + assertConditionallyCached(new MockResponse().addHeader("ETag: v1")); + assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); + } + + @Test public void etagAndExpirationDateInThePast() 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 headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-None-Match: v1")); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void etagAndExpirationDateInTheFuture() throws Exception { + assertFullyCached(new MockResponse().addHeader("ETag: v1") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + } + + @Test public void cacheControlNoCache() throws Exception { + assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); + } + + @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() 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 headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void pragmaNoCache() throws Exception { + assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); + } + + @Test public void pragmaNoCacheAndExpirationDateInTheFuture() 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 headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + @Test public void cacheControlNoStore() throws Exception { + assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); + } + + @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { + assertNotCached(new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Cache-Control: no-store")); + } + + @Test public void partialRangeResponsesDoNotCorruptCache() 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")); + + URL url = server.getUrl("/"); + + URLConnection range = client.open(url); + range.addRequestProperty("Range", "bytes=1000-1001"); + assertEquals("AA", readAscii(range)); + + assertEquals("BB", readAscii(client.open(url))); + } + + @Test public void serverReturnsDocumentOlderThanCache() 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))); + + URL url = server.getUrl("/"); + + assertEquals("A", readAscii(client.open(url))); + assertEquals("A", readAscii(client.open(url))); + } + + @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { + assertNonIdentityEncodingCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + } + + @Test public void nonIdentityEncodingAndFullCache() 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")).addHeader("Content-Encoding: gzip")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + // At least three request/response pairs are required because after the first request is cached + // a different execution path might be taken. Thus modifications to the cache applied during + // the second request might not be visible until another request is performed. + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void notModifiedSpecifiesEncoding() throws Exception { + server.enqueue(new MockResponse() + .setBody(gzip("ABCABCABC")) + .addHeader("Content-Encoding: gzip") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED) + .addHeader("Content-Encoding: gzip")); + server.enqueue(new MockResponse() + .setBody("DEFDEFDEF")); + + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("DEFDEFDEF", readAscii(client.open(server.getUrl("/")))); + } + + /** https://github.com/square/okhttp/issues/947 */ + @Test public void gzipAndVaryOnAcceptEncoding() throws Exception { + server.enqueue(new MockResponse() + .setBody(gzip("ABCABCABC")) + .addHeader("Content-Encoding: gzip") + .addHeader("Vary: Accept-Encoding") + .addHeader("Cache-Control: max-age=60")); + server.enqueue(new MockResponse().setBody("FAIL")); + + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception { + server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A")); + server.enqueue(new MockResponse() + .clearHeaders() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + ConnectionPool pool = ConnectionPool.getDefault(); + pool.evictAll(); + client.client().setConnectionPool(pool); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(1, client.client().getConnectionPool().getConnectionCount()); + } + + @Test public void expiresDateBeforeModifiedDate() throws Exception { + assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); + } + + @Test public void requestMaxAge() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-age=30"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestMinFresh() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "min-fresh=120"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestMaxStale() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-stale=180"); + assertEquals("A", readAscii(connection)); + assertEquals("110 HttpURLConnection \"Response is stale\"", + connection.getHeaderField("Warning")); + } + + @Test public void requestMaxStaleNotHonoredWithMustRevalidate() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-stale=180"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { + // (no responses enqueued) + + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + assertEquals(1, cache.getRequestCount()); + assertEquals(0, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + } + + @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertEquals("A", readAscii(connection)); + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(1, cache.getHitCount()); + } + + @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + } + + @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + } + + @Test public void requestCacheControlNoCache() 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")); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + URLConnection connection = client.open(url); + connection.setRequestProperty("Cache-Control", "no-cache"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestPragmaNoCache() 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")); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + URLConnection connection = client.open(url); + connection.setRequestProperty("Pragma", "no-cache"); + assertEquals("B", readAscii(connection)); + } + + @Test public void clientSuppliedIfModifiedSinceWithCachedResult() 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 headers = request.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); + assertFalse(headers.contains("If-None-Match: v3")); + } + + @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() 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 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)); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + + HttpURLConnection connection = client.open(url); + connection.addRequestProperty(conditionName, conditionValue); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); + assertEquals("", readAscii(connection)); + + server.takeRequest(); // seed + return server.takeRequest(); + } + + /** + * Confirm that {@link URLConnection#setIfModifiedSince} causes an + * If-Modified-Since header with a GMT timestamp. + * + * https://code.google.com/p/android/issues/detail?id=66135 + */ + @Test public void setIfModifiedSince() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + + URL url = server.getUrl("/"); + URLConnection connection = client.open(url); + connection.setIfModifiedSince(1393666200000L); + assertEquals("A", readAscii(connection)); + RecordedRequest request = server.takeRequest(); + String ifModifiedSinceHeader = request.getHeader("If-Modified-Since"); + assertEquals("Sat, 01 Mar 2014 09:30:00 GMT", ifModifiedSinceHeader); + } + + /** + * For Last-Modified and Date headers, we should echo the date back in the + * exact format we were served. + */ + @Test public void retainServedDateFormat() throws Exception { + // Serve a response with a non-standard date format that OkHttp supports. + Date lastModifiedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1)); + Date servedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2)); + DateFormat dateFormat = new SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("EDT")); + String lastModifiedString = dateFormat.format(lastModifiedDate); + String servedString = dateFormat.format(servedDate); + + // This response should be conditionally cached. + server.enqueue(new MockResponse() + .addHeader("Last-Modified: " + lastModifiedString) + .addHeader("Expires: " + servedString) + .setBody("A")); + server.enqueue(new MockResponse() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + + // The first request has no conditions. + RecordedRequest request1 = server.takeRequest(); + assertNull(request1.getHeader("If-Modified-Since")); + + // The 2nd request uses the server's date format. + RecordedRequest request2 = server.takeRequest(); + assertEquals(lastModifiedString, request2.getHeader("If-Modified-Since")); + } + + @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + HttpURLConnection connection = client.open(server.getUrl("/")); + String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); + connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); + assertEquals("", readAscii(connection)); + } + + @Test public void authorizationRequestHeaderPreventsCaching() 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")); + + URL url = server.getUrl("/"); + URLConnection connection = client.open(url); + connection.addRequestProperty("Authorization", "password"); + assertEquals("A", readAscii(connection)); + assertEquals("B", readAscii(client.open(url))); + } + + @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { + assertAuthorizationRequestFullyCached( + new MockResponse().addHeader("Cache-Control: s-maxage=60")); + } + + @Test public void authorizationResponseCachedWithPublic() throws Exception { + assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public")); + } + + @Test public void authorizationResponseCachedWithMustRevalidate() 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")); + + URL url = server.getUrl("/"); + URLConnection connection = client.open(url); + connection.addRequestProperty("Authorization", "password"); + assertEquals("A", readAscii(connection)); + assertEquals("A", readAscii(client.open(url))); + } + + @Test public void contentLocationDoesNotPopulateCache() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Content-Location: /bar") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + assertEquals("A", readAscii(client.open(server.getUrl("/foo")))); + assertEquals("B", readAscii(client.open(server.getUrl("/bar")))); + } + + @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URLConnection connection = client.open(server.getUrl("/")); + connection.setUseCaches(false); + assertEquals("A", readAscii(connection)); + assertEquals("B", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + URLConnection connection = client.open(server.getUrl("/")); + connection.setUseCaches(false); + assertEquals("B", readAscii(connection)); + } + + @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { + URL url = new URL("http://localhost/"); + URLConnection c1 = client.open(url); + URLConnection c2 = client.open(url); + assertTrue(c1.getDefaultUseCaches()); + c1.setDefaultUseCaches(false); + try { + assertTrue(c1.getUseCaches()); + assertTrue(c2.getUseCaches()); + URLConnection c3 = client.open(url); + assertFalse(c3.getUseCaches()); + } finally { + c1.setDefaultUseCaches(true); + } + } + + @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/a")))); + assertEquals("A", readAscii(client.open(server.getUrl("/a")))); + assertEquals("B", readAscii(client.open(server.getUrl("/b")))); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + @Test public void statisticsConditionalCacheMiss() 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")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("B", readAscii(client.open(server.getUrl("/")))); + assertEquals("C", readAscii(client.open(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(3, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + } + + @Test public void statisticsConditionalCacheHit() 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)); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(3, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void statisticsFullCacheHit() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void varyMatchesChangedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + HttpURLConnection frConnection = client.open(url); + frConnection.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(frConnection)); + + HttpURLConnection enConnection = client.open(url); + enConnection.addRequestProperty("Accept-Language", "en-US"); + assertEquals("B", readAscii(enConnection)); + } + + @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + URLConnection connection1 = client.open(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = client.open(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMatchesAbsentRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void varyMatchesAddedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + URLConnection fooConnection = client.open(server.getUrl("/")); + fooConnection.addRequestProperty("Foo", "bar"); + assertEquals("B", readAscii(fooConnection)); + } + + @Test public void varyMatchesRemovedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URLConnection fooConnection = client.open(server.getUrl("/")); + fooConnection.addRequestProperty("Foo", "bar"); + assertEquals("A", readAscii(fooConnection)); + assertEquals("B", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void varyFieldsAreCaseInsensitive() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: ACCEPT-LANGUAGE") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + URLConnection connection1 = client.open(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = client.open(url); + connection2.addRequestProperty("accept-language", "fr-CA"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldsWithMatch() 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")); + + URL url = server.getUrl("/"); + URLConnection connection1 = client.open(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + connection1.addRequestProperty("Accept-Charset", "UTF-8"); + connection1.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = client.open(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + connection2.addRequestProperty("Accept-Charset", "UTF-8"); + connection2.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldsWithNoMatch() 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")); + + URL url = server.getUrl("/"); + URLConnection frConnection = client.open(url); + frConnection.addRequestProperty("Accept-Language", "fr-CA"); + frConnection.addRequestProperty("Accept-Charset", "UTF-8"); + frConnection.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(frConnection)); + URLConnection enConnection = client.open(url); + enConnection.addRequestProperty("Accept-Language", "en-CA"); + enConnection.addRequestProperty("Accept-Charset", "UTF-8"); + enConnection.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("B", readAscii(enConnection)); + } + + @Test public void varyMultipleFieldValuesWithMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + URLConnection connection1 = client.open(url); + connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + URLConnection connection2 = client.open(url); + connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + URLConnection connection1 = client.open(url); + connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + URLConnection connection2 = client.open(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("B", readAscii(connection2)); + } + + @Test public void varyAsterisk() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: *") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + assertEquals("B", readAscii(client.open(server.getUrl("/")))); + } + + @Test public void varyAndHttps() throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + + URL url = server.getUrl("/"); + HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url); + connection1.setSSLSocketFactory(sslContext.getSocketFactory()); + connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url); + connection2.setSSLSocketFactory(sslContext.getSocketFactory()); + connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void cachePlusCookies() 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)); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + assertCookies(url, "a=FIRST"); + assertEquals("A", readAscii(client.open(url))); + assertCookies(url, "a=SECOND"); + } + + @Test public void getHeadersReturnsNetworkEndToEndHeaders() 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)); + + URLConnection connection1 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); + + URLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); + } + + @Test public void getHeadersReturnsCachedHopByHopHeaders() 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)); + + URLConnection connection1 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); + + URLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); + } + + @Test public void getHeadersDeletesCached100LevelWarnings() 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)); + + URLConnection connection1 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("199 test danger", connection1.getHeaderField("Warning")); + + URLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals(null, connection2.getHeaderField("Warning")); + } + + @Test public void getHeadersRetainsCached200LevelWarnings() 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)); + + URLConnection connection1 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("299 test danger", connection1.getHeaderField("Warning")); + + URLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("299 test danger", connection2.getHeaderField("Warning")); + } + + public void assertCookies(URL url, String... expectedCookies) throws Exception { + List actualCookies = new ArrayList<>(); + for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { + actualCookies.add(cookie.toString()); + } + assertEquals(Arrays.asList(expectedCookies), actualCookies); + } + + @Test public void cachePlusRange() 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")); + } + + @Test public void conditionalHitUpdatesCache() 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")); + + // cache miss; seed the cache + HttpURLConnection connection1 = client.open(server.getUrl("/a")); + assertEquals("A", readAscii(connection1)); + assertEquals(null, connection1.getHeaderField("Allow")); + + // conditional cache hit; update the cache + HttpURLConnection connection2 = client.open(server.getUrl("/a")); + assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); + assertEquals("A", readAscii(connection2)); + assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); + + // full cache hit + HttpURLConnection connection3 = client.open(server.getUrl("/a")); + assertEquals("A", readAscii(connection3)); + assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); + + assertEquals(2, server.getRequestCount()); + } + + @Test public void responseSourceHeaderCached() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertEquals("A", readAscii(connection)); + } + + @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setBody("B") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("B", readAscii(connection)); + } + + @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=0") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setResponseCode(304)); + + assertEquals("A", readAscii(client.open(server.getUrl("/")))); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection)); + } + + @Test public void responseSourceHeaderFetched() throws IOException { + server.enqueue(new MockResponse().setBody("A")); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection)); + } + + @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception { + server.enqueue(new MockResponse() + .addHeader("Cache-Control: max-age=120") + .addHeader(": A") + .setBody("body")); + + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", connection.getHeaderField("")); + } + + /** + * Old implementations of OkHttp's response cache wrote header fields like + * ":status: 200 OK". This broke our cached response parser because it split + * on the first colon. This regression test exists to help us read these old + * bad cache entries. + * + * https://github.com/square/okhttp/issues/227 + */ + @Test public void testGoldenCacheResponse() throws Exception { + cache.close(); + server.enqueue(new MockResponse() + .clearHeaders() + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + URL url = server.getUrl("/"); + String urlKey = Util.hash(url.toString()); + String entryMetadata = "" + + "" + url + "\n" + + "GET\n" + + "0\n" + + "HTTP/1.1 200 OK\n" + + "7\n" + + ":status: 200 OK\n" + + ":version: HTTP/1.1\n" + + "etag: foo\n" + + "content-length: 3\n" + + "OkHttp-Received-Millis: " + System.currentTimeMillis() + "\n" + + "X-Android-Response-Source: NETWORK 200\n" + + "OkHttp-Sent-Millis: " + System.currentTimeMillis() + "\n" + + "\n" + + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n" + + "1\n" + + "MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgy" + + "OTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEF" + + "AAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUc" + + "ltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFK" + + "v6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP" + + "7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dm" + + "yFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg==\n" + + "-1\n"; + String entryBody = "abc"; + String journalBody = "" + + "libcore.io.DiskLruCache\n" + + "1\n" + + "201105\n" + + "2\n" + + "\n" + + "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n"; + writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata); + writeFile(cache.getDirectory(), urlKey + ".1", entryBody); + writeFile(cache.getDirectory(), "journal", journalBody); + cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE); + client.client().setCache(cache); + + HttpURLConnection connection = client.open(url); + assertEquals(entryBody, readAscii(connection)); + assertEquals("3", connection.getHeaderField("Content-Length")); + assertEquals("foo", connection.getHeaderField("etag")); + } + + private void writeFile(File directory, String file, String content) throws IOException { + BufferedSink sink = Okio.buffer(Okio.sink(new File(directory, file))); + sink.writeUtf8(content); + sink.close(); + } + + /** + * @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("GMT")); + 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")); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + assertEquals("B", readAscii(client.open(url))); + } + + /** @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")); + + URL valid = server.getUrl("/valid"); + HttpURLConnection connection1 = client.open(valid); + assertEquals("A", readAscii(connection1)); + assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); + assertEquals("A-OK", connection1.getResponseMessage()); + HttpURLConnection connection2 = client.open(valid); + assertEquals("A", readAscii(connection2)); + assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); + assertEquals("A-OK", connection2.getResponseMessage()); + + URL invalid = server.getUrl("/invalid"); + HttpURLConnection connection3 = client.open(invalid); + assertEquals("B", readAscii(connection3)); + assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); + assertEquals("B-OK", connection3.getResponseMessage()); + HttpURLConnection connection4 = client.open(invalid); + 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")); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(client.open(url))); + assertEquals("A", readAscii(client.open(url))); + } + + /** + * 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 headers = new ArrayList<>(response.getHeaders()); + Buffer truncatedBody = new Buffer(); + truncatedBody.write(response.getBody(), numBytesToKeep); + response.setBody(truncatedBody); + 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, Buffer content, int chunkSize) + throws IOException { + response.setChunkedBody(content, chunkSize); + } + }, + FIXED_LENGTH() { + @Override void setBody(MockResponse response, Buffer content, int chunkSize) { + response.setBody(content); + } + }, + END_OF_STREAM() { + @Override void setBody(MockResponse response, Buffer content, int chunkSize) { + response.setBody(content); + response.setSocketPolicy(DISCONNECT_AT_END); + for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { + if (h.next().startsWith("Content-Length:")) { + h.remove(); + break; + } + } + } + }; + + abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException; + + void setBody(MockResponse response, String content, int chunkSize) throws IOException { + setBody(response, new Buffer().writeUtf8(content), chunkSize); + } + } + + private List toListOrNull(T[] arrayOrNull) { + return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; + } + + /** Returns a gzipped copy of {@code bytes}. */ + public Buffer gzip(String data) throws IOException { + Buffer result = new Buffer(); + BufferedSink sink = Okio.buffer(new GzipSink(result)); + sink.writeUtf8(data); + sink.close(); + return result; + } +} diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java similarity index 100% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java similarity index 97% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java index 4a4befeb060f..5f01af5621e1 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java +++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.squareup.okhttp.internal.huc; import com.squareup.okhttp.Handshake; @@ -72,9 +71,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -/** - * Tests for {@link JavaApiConverter}. - */ public class JavaApiConverterTest { // $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \ @@ -214,16 +210,14 @@ private void testCreateOkResponseInternal(HttpURLConnectionFactory httpUrlConnec URI uri = new URI("http://foo/bar"); Request request = new Request.Builder().url(uri.toURL()).build(); CacheResponse cacheResponse = new CacheResponse() { - @Override - public Map> getHeaders() throws IOException { + @Override public Map> getHeaders() throws IOException { Map> headers = new HashMap<>(); headers.put(null, Collections.singletonList(statusLine)); headers.put("xyzzy", Arrays.asList("bar", "baz")); return headers; } - @Override - public InputStream getBody() throws IOException { + @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8)); } }; @@ -249,41 +243,34 @@ public InputStream getBody() throws IOException { URI uri = new URI("https://foo/bar"); Request request = new Request.Builder().url(uri.toURL()).build(); SecureCacheResponse cacheResponse = new SecureCacheResponse() { - @Override - public Map> getHeaders() throws IOException { + @Override public Map> getHeaders() throws IOException { Map> headers = new HashMap<>(); headers.put(null, Collections.singletonList(statusLine)); headers.put("xyzzy", Arrays.asList("bar", "baz")); return headers; } - @Override - public InputStream getBody() throws IOException { + @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8)); } - @Override - public String getCipherSuite() { + @Override public String getCipherSuite() { return "SuperSecure"; } - @Override - public List getLocalCertificateChain() { + @Override public List getLocalCertificateChain() { return localCertificates; } - @Override - public List getServerCertificateChain() throws SSLPeerUnverifiedException { + @Override public List getServerCertificateChain() throws SSLPeerUnverifiedException { return serverCertificates; } - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { return serverPrincipal; } - @Override - public Principal getLocalPrincipal() { + @Override public Principal getLocalPrincipal() { return localPrincipal; } }; @@ -725,15 +712,13 @@ private OkHttpURLConnectionFactory(OkHttpClient client) { this.client = client; } - @Override - public HttpURLConnection open(URL serverUrl) { + @Override public HttpURLConnection open(URL serverUrl) { return new OkUrlFactory(client).open(serverUrl); } } private static class JavaHttpURLConnectionFactory implements HttpURLConnectionFactory { - @Override - public HttpURLConnection open(URL serverUrl) throws IOException { + @Override public HttpURLConnection open(URL serverUrl) throws IOException { return (HttpURLConnection) serverUrl.openConnection(); } } diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java similarity index 95% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java index 381482affea5..5fc68d4f3896 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java +++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java @@ -55,6 +55,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -78,12 +80,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -/** - * Tests for interaction between OkHttp and the ResponseCache. This test is - * based on {@link com.squareup.okhttp.CacheTest}. Some tests for the {@link - * com.squareup.okhttp.internal.InternalCache} in CacheTest cover ResponseCache - * as well. - */ +/** Tests the interaction between OkHttp and {@link ResponseCache}. */ public final class ResponseCacheTest { private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { @@ -570,10 +567,9 @@ private void testRequestMethod(String requestMethod, boolean expectCached) throw } /** - * Equivalent to {@link com.squareup.okhttp.CacheTest#postInvalidatesCacheWithUncacheableResponse()} but - * demonstrating that {@link ResponseCache} provides no mechanism for cache invalidation as the - * result of locally-made requests. In reality invalidation could take place from other clients at - * any time. + * Equivalent to {@code CacheTest.postInvalidatesCacheWithUncacheableResponse()} but demonstrating + * that {@link ResponseCache} provides no mechanism for cache invalidation as the result of + * locally-made requests. In reality invalidation could take place from other clients at any time. */ @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception { // 1. seed the cache @@ -1159,9 +1155,8 @@ public void assertCookies(CookieManager cookieManager, URL url, String... expect } /** - * Equivalent to {@link com.squareup.okhttp.CacheTest#conditionalHitUpdatesCache()}, except a Java - * standard cache has no means to update the headers for an existing entry so the behavior is - * different. + * Equivalent to {@code CacheTest.conditionalHitUpdatesCache()}, except a Java standard cache has + * no means to update the headers for an existing entry so the behavior is different. */ @Test public void conditionalHitDoesNotUpdateCache() throws Exception { // A response that is cacheable, but with a short life. @@ -1253,6 +1248,69 @@ public void assertCookies(CookieManager cookieManager, URL url, String... expect assertEquals("A", connection.getHeaderField("")); } + /** + * Test that we can interrogate the response when the cache is being + * populated. http://code.google.com/p/android/issues/detail?id=7787 + */ + @Test public void responseCacheCallbackApis() throws Exception { + final String body = "ABCDE"; + final AtomicInteger cacheCount = new AtomicInteger(); + + server.enqueue(new MockResponse() + .setStatus("HTTP/1.1 200 Fantastic") + .addHeader("Content-Type: text/plain") + .addHeader("fgh: ijk") + .setBody(body)); + + Internal.instance.setCache(client, new CacheAdapter(new AbstractResponseCache() { + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + HttpURLConnection httpURLConnection = (HttpURLConnection) connection; + assertEquals(server.getUrl("/"), uri.toURL()); + assertEquals(200, httpURLConnection.getResponseCode()); + try { + httpURLConnection.getInputStream(); + fail(); + } catch (UnsupportedOperationException expected) { + } + assertEquals("5", connection.getHeaderField("Content-Length")); + assertEquals("text/plain", connection.getHeaderField("Content-Type")); + assertEquals("ijk", connection.getHeaderField("fgh")); + cacheCount.incrementAndGet(); + return null; + } + })); + + URL url = server.getUrl("/"); + HttpURLConnection connection = openConnection(url); + assertEquals(body, readAscii(connection)); + assertEquals(1, cacheCount.get()); + } + + /** Don't explode if the cache returns a null body. http://b/3373699 */ + @Test public void responseCacheReturnsNullOutputStream() throws Exception { + final AtomicBoolean aborted = new AtomicBoolean(); + Internal.instance.setCache(client, new CacheAdapter(new AbstractResponseCache() { + @Override public CacheRequest put(URI uri, URLConnection connection) { + return new CacheRequest() { + @Override public void abort() { + aborted.set(true); + } + + @Override public OutputStream getBody() throws IOException { + return null; + } + }; + } + })); + + server.enqueue(new MockResponse().setBody("abcdef")); + + HttpURLConnection connection = openConnection(server.getUrl("/")); + assertEquals("abc", readAscii(connection, 3)); + connection.getInputStream().close(); + assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here + } + /** * @param delta the offset from the current date to use. Negative * values yield dates in the past; positive values yield dates in the @@ -1331,7 +1389,7 @@ private void assertFullyCached(MockResponse response) throws Exception { */ private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { response.setSocketPolicy(DISCONNECT_AT_END); - List headers = new ArrayList(response.getHeaders()); + List headers = new ArrayList<>(response.getHeaders()); Buffer truncatedBody = new Buffer(); truncatedBody.write(response.getBody(), numBytesToKeep); response.setBody(truncatedBody); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java similarity index 98% rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java index 547f009b6773..4c5f28c8bf7e 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java +++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package com.squareup.okhttp.internal.http; +package com.squareup.okhttp.internal.huc; import com.squareup.okhttp.AbstractResponseCache; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkUrlFactory; import com.squareup.okhttp.internal.Internal; -import com.squareup.okhttp.internal.huc.CacheAdapter; import java.io.IOException; import java.net.CacheResponse; import java.net.HttpURLConnection; diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java index b10dea08763e..affc99d07417 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java @@ -388,8 +388,10 @@ private class AbstractSource { /** Copy the last {@code byteCount} bytes of {@code source} to the cache body. */ protected final void cacheWrite(Buffer source, long byteCount) throws IOException { if (cacheBody != null) { - // TODO source.copyTo(cacheBody, byteCount); - cacheBody.write(source.clone(), byteCount); + // TODO source.copyTo(cacheBody, source.size() - byteCount, byteCount) + Buffer sourceCopy = source.clone(); + sourceCopy.skip(sourceCopy.size() - byteCount); + cacheBody.write(sourceCopy, byteCount); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 4af0fa2eca52..b6710575f037 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -733,8 +733,8 @@ private static Headers combine(Headers cachedHeaders, Headers networkHeaders) th for (int i = 0; i < cachedHeaders.size(); i++) { String fieldName = cachedHeaders.name(i); String value = cachedHeaders.value(i); - if ("Warning".equals(fieldName) && value.startsWith("1")) { - continue; // drop 100-level freshness warnings + if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) { + continue; // Drop 100-level freshness warnings. } if (!OkHeaders.isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) { result.add(fieldName, value); @@ -743,6 +743,9 @@ private static Headers combine(Headers cachedHeaders, Headers networkHeaders) th for (int i = 0; i < networkHeaders.size(); i++) { String fieldName = networkHeaders.name(i); + if ("Content-Length".equalsIgnoreCase(fieldName)) { + continue; // Ignore content-length headers of validating responses. + } if (OkHeaders.isEndToEnd(fieldName)) { result.add(fieldName, networkHeaders.value(i)); }