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)); }