HttpResponseCacheTest.java revision c6bd683320121544811f481709b3fdbcbe9b3866
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.squareup.okhttp.internal.http; 18 19import com.squareup.okhttp.ConnectionPool; 20import com.squareup.okhttp.HttpResponseCache; 21import com.squareup.okhttp.OkHttpClient; 22import com.squareup.okhttp.OkResponseCache; 23import com.squareup.okhttp.Request; 24import com.squareup.okhttp.Response; 25import com.squareup.okhttp.ResponseSource; 26import com.squareup.okhttp.internal.SslContextBuilder; 27import com.squareup.okhttp.internal.Util; 28import com.squareup.okhttp.mockwebserver.MockResponse; 29import com.squareup.okhttp.mockwebserver.MockWebServer; 30import com.squareup.okhttp.mockwebserver.RecordedRequest; 31import java.io.BufferedReader; 32import java.io.ByteArrayOutputStream; 33import java.io.File; 34import java.io.FileNotFoundException; 35import java.io.FileOutputStream; 36import java.io.IOException; 37import java.io.InputStream; 38import java.io.InputStreamReader; 39import java.io.OutputStream; 40import java.net.CacheRequest; 41import java.net.CookieHandler; 42import java.net.CookieManager; 43import java.net.HttpCookie; 44import java.net.HttpURLConnection; 45import java.net.ResponseCache; 46import java.net.URISyntaxException; 47import java.net.URL; 48import java.net.URLConnection; 49import java.security.Principal; 50import java.security.cert.Certificate; 51import java.text.DateFormat; 52import java.text.SimpleDateFormat; 53import java.util.ArrayList; 54import java.util.Arrays; 55import java.util.Date; 56import java.util.Iterator; 57import java.util.List; 58import java.util.Locale; 59import java.util.TimeZone; 60import java.util.UUID; 61import java.util.concurrent.TimeUnit; 62import java.util.concurrent.atomic.AtomicBoolean; 63import java.util.concurrent.atomic.AtomicInteger; 64import java.util.concurrent.atomic.AtomicReference; 65import java.util.zip.GZIPOutputStream; 66import javax.net.ssl.HostnameVerifier; 67import javax.net.ssl.HttpsURLConnection; 68import javax.net.ssl.SSLContext; 69import javax.net.ssl.SSLSession; 70import org.junit.After; 71import org.junit.Before; 72import org.junit.Test; 73 74import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END; 75import static org.junit.Assert.assertEquals; 76import static org.junit.Assert.assertFalse; 77import static org.junit.Assert.assertNotNull; 78import static org.junit.Assert.assertNull; 79import static org.junit.Assert.assertSame; 80import static org.junit.Assert.assertTrue; 81import static org.junit.Assert.fail; 82 83/** 84 * Android's HttpResponseCacheTest. This tests both the {@link HttpResponseCache} implementation and 85 * the behavior of {@link com.squareup.okhttp.OkResponseCache} classes generally. 86 */ 87public final class HttpResponseCacheTest { 88 private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { 89 @Override public boolean verify(String s, SSLSession sslSession) { 90 return true; 91 } 92 }; 93 94 private static final SSLContext sslContext = SslContextBuilder.localhost(); 95 96 private final OkHttpClient client = new OkHttpClient(); 97 private MockWebServer server = new MockWebServer(); 98 private MockWebServer server2 = new MockWebServer(); 99 private HttpResponseCache cache; 100 private final CookieManager cookieManager = new CookieManager(); 101 102 @Before public void setUp() throws Exception { 103 String tmp = System.getProperty("java.io.tmpdir"); 104 File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); 105 cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); 106 ResponseCache.setDefault(cache); 107 CookieHandler.setDefault(cookieManager); 108 server.setNpnEnabled(false); 109 } 110 111 @After public void tearDown() throws Exception { 112 server.shutdown(); 113 server2.shutdown(); 114 ResponseCache.setDefault(null); 115 cache.delete(); 116 CookieHandler.setDefault(null); 117 } 118 119 private HttpURLConnection openConnection(URL url) { 120 return client.open(url); 121 } 122 123 @Test public void responseCacheAccessWithOkHttpMember() throws IOException { 124 ResponseCache.setDefault(null); 125 client.setResponseCache(cache); 126 assertSame(cache, client.getOkResponseCache()); 127 assertNull(client.getResponseCache()); 128 } 129 130 @Test public void responseCacheAccessWithGlobalDefault() throws IOException { 131 ResponseCache.setDefault(cache); 132 client.setResponseCache(null); 133 assertNull(client.getOkResponseCache()); 134 assertNull(client.getResponseCache()); 135 } 136 137 /** 138 * Test that response caching is consistent with the RI and the spec. 139 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 140 */ 141 @Test public void responseCachingByResponseCode() throws Exception { 142 // Test each documented HTTP/1.1 code, plus the first unused value in each range. 143 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 144 145 // We can't test 100 because it's not really a response. 146 // assertCached(false, 100); 147 assertCached(false, 101); 148 assertCached(false, 102); 149 assertCached(true, 200); 150 assertCached(false, 201); 151 assertCached(false, 202); 152 assertCached(true, 203); 153 assertCached(false, 204); 154 assertCached(false, 205); 155 assertCached(false, 206); // we don't cache partial responses 156 assertCached(false, 207); 157 assertCached(true, 300); 158 assertCached(true, 301); 159 for (int i = 302; i <= 308; ++i) { 160 assertCached(false, i); 161 } 162 for (int i = 400; i <= 406; ++i) { 163 assertCached(false, i); 164 } 165 // (See test_responseCaching_407.) 166 assertCached(false, 408); 167 assertCached(false, 409); 168 // (See test_responseCaching_410.) 169 for (int i = 411; i <= 418; ++i) { 170 assertCached(false, i); 171 } 172 for (int i = 500; i <= 506; ++i) { 173 assertCached(false, i); 174 } 175 } 176 177 /** 178 * Response code 407 should only come from proxy servers. Android's client 179 * throws if it is sent by an origin server. 180 */ 181 @Test public void originServerSends407() throws Exception { 182 server.enqueue(new MockResponse().setResponseCode(407)); 183 server.play(); 184 185 URL url = server.getUrl("/"); 186 HttpURLConnection conn = openConnection(url); 187 try { 188 conn.getResponseCode(); 189 fail(); 190 } catch (IOException expected) { 191 } 192 } 193 194 @Test public void responseCaching_410() throws Exception { 195 // the HTTP spec permits caching 410s, but the RI doesn't. 196 assertCached(true, 410); 197 } 198 199 private void assertCached(boolean shouldPut, int responseCode) throws Exception { 200 server = new MockWebServer(); 201 MockResponse response = 202 new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 203 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 204 .setResponseCode(responseCode) 205 .setBody("ABCDE") 206 .addHeader("WWW-Authenticate: challenge"); 207 if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) { 208 response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); 209 } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { 210 response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); 211 } 212 server.enqueue(response); 213 server.play(); 214 215 URL url = server.getUrl("/"); 216 HttpURLConnection conn = openConnection(url); 217 assertEquals(responseCode, conn.getResponseCode()); 218 219 // exhaust the content stream 220 readAscii(conn); 221 222 Response cached = cache.get(new Request.Builder().url(url).build()); 223 if (shouldPut) { 224 assertNotNull(Integer.toString(responseCode), cached); 225 cached.body().close(); 226 } else { 227 assertNull(Integer.toString(responseCode), cached); 228 } 229 server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers 230 } 231 232 /** 233 * Test that we can interrogate the response when the cache is being 234 * populated. http://code.google.com/p/android/issues/detail?id=7787 235 */ 236 @Test public void responseCacheCallbackApis() throws Exception { 237 final String body = "ABCDE"; 238 final AtomicInteger cacheCount = new AtomicInteger(); 239 240 server.enqueue(new MockResponse() 241 .setStatus("HTTP/1.1 200 Fantastic") 242 .addHeader("Content-Type: text/plain") 243 .addHeader("fgh: ijk") 244 .setBody(body)); 245 server.play(); 246 247 client.setOkResponseCache(new AbstractOkResponseCache() { 248 @Override public CacheRequest put(Response response) throws IOException { 249 assertEquals(server.getUrl("/"), response.request().url()); 250 assertEquals(200, response.code()); 251 assertNull(response.body()); 252 assertEquals("5", response.header("Content-Length")); 253 assertEquals("text/plain", response.header("Content-Type")); 254 assertEquals("ijk", response.header("fgh")); 255 cacheCount.incrementAndGet(); 256 return null; 257 } 258 }); 259 260 URL url = server.getUrl("/"); 261 HttpURLConnection connection = openConnection(url); 262 assertEquals(body, readAscii(connection)); 263 assertEquals(1, cacheCount.get()); 264 } 265 266 /** Don't explode if the cache returns a null body. http://b/3373699 */ 267 @Test public void responseCacheReturnsNullOutputStream() throws Exception { 268 final AtomicBoolean aborted = new AtomicBoolean(); 269 client.setOkResponseCache(new AbstractOkResponseCache() { 270 @Override public CacheRequest put(Response response) throws IOException { 271 return new CacheRequest() { 272 @Override public void abort() { 273 aborted.set(true); 274 } 275 276 @Override public OutputStream getBody() throws IOException { 277 return null; 278 } 279 }; 280 } 281 }); 282 283 server.enqueue(new MockResponse().setBody("abcdef")); 284 server.play(); 285 286 HttpURLConnection connection = client.open(server.getUrl("/")); 287 assertEquals("abc", readAscii(connection, 3)); 288 connection.getInputStream().close(); 289 assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here 290 } 291 292 @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { 293 testResponseCaching(TransferKind.FIXED_LENGTH); 294 } 295 296 @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { 297 testResponseCaching(TransferKind.CHUNKED); 298 } 299 300 @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { 301 testResponseCaching(TransferKind.END_OF_STREAM); 302 } 303 304 /** 305 * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption 306 * http://code.google.com/p/android/issues/detail?id=8175 307 */ 308 private void testResponseCaching(TransferKind transferKind) throws IOException { 309 MockResponse response = 310 new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 311 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 312 .setStatus("HTTP/1.1 200 Fantastic"); 313 transferKind.setBody(response, "I love puppies but hate spiders", 1); 314 server.enqueue(response); 315 server.play(); 316 317 // Make sure that calling skip() doesn't omit bytes from the cache. 318 HttpURLConnection urlConnection = openConnection(server.getUrl("/")); 319 InputStream in = urlConnection.getInputStream(); 320 assertEquals("I love ", readAscii(urlConnection, "I love ".length())); 321 reliableSkip(in, "puppies but hate ".length()); 322 assertEquals("spiders", readAscii(urlConnection, "spiders".length())); 323 assertEquals(-1, in.read()); 324 in.close(); 325 assertEquals(1, cache.getWriteSuccessCount()); 326 assertEquals(0, cache.getWriteAbortCount()); 327 328 urlConnection = openConnection(server.getUrl("/")); // cached! 329 in = urlConnection.getInputStream(); 330 assertEquals("I love puppies but hate spiders", 331 readAscii(urlConnection, "I love puppies but hate spiders".length())); 332 assertEquals(200, urlConnection.getResponseCode()); 333 assertEquals("Fantastic", urlConnection.getResponseMessage()); 334 335 assertEquals(-1, in.read()); 336 in.close(); 337 assertEquals(1, cache.getWriteSuccessCount()); 338 assertEquals(0, cache.getWriteAbortCount()); 339 assertEquals(2, cache.getRequestCount()); 340 assertEquals(1, cache.getHitCount()); 341 } 342 343 @Test public void secureResponseCaching() throws IOException { 344 server.useHttps(sslContext.getSocketFactory(), false); 345 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 346 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 347 .setBody("ABC")); 348 server.play(); 349 350 HttpsURLConnection c1 = (HttpsURLConnection) client.open(server.getUrl("/")); 351 c1.setSSLSocketFactory(sslContext.getSocketFactory()); 352 c1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 353 assertEquals("ABC", readAscii(c1)); 354 355 // OpenJDK 6 fails on this line, complaining that the connection isn't open yet 356 String suite = c1.getCipherSuite(); 357 List<Certificate> localCerts = toListOrNull(c1.getLocalCertificates()); 358 List<Certificate> serverCerts = toListOrNull(c1.getServerCertificates()); 359 Principal peerPrincipal = c1.getPeerPrincipal(); 360 Principal localPrincipal = c1.getLocalPrincipal(); 361 362 HttpsURLConnection c2 = (HttpsURLConnection) client.open(server.getUrl("/")); // cached! 363 c2.setSSLSocketFactory(sslContext.getSocketFactory()); 364 c2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 365 assertEquals("ABC", readAscii(c2)); 366 367 assertEquals(2, cache.getRequestCount()); 368 assertEquals(1, cache.getNetworkCount()); 369 assertEquals(1, cache.getHitCount()); 370 371 assertEquals(suite, c2.getCipherSuite()); 372 assertEquals(localCerts, toListOrNull(c2.getLocalCertificates())); 373 assertEquals(serverCerts, toListOrNull(c2.getServerCertificates())); 374 assertEquals(peerPrincipal, c2.getPeerPrincipal()); 375 assertEquals(localPrincipal, c2.getLocalPrincipal()); 376 } 377 378 @Test public void responseCachingAndRedirects() throws Exception { 379 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 380 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 381 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 382 .addHeader("Location: /foo")); 383 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 384 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 385 .setBody("ABC")); 386 server.enqueue(new MockResponse().setBody("DEF")); 387 server.play(); 388 389 HttpURLConnection connection = openConnection(server.getUrl("/")); 390 assertEquals("ABC", readAscii(connection)); 391 392 connection = openConnection(server.getUrl("/")); // cached! 393 assertEquals("ABC", readAscii(connection)); 394 395 assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects 396 assertEquals(2, cache.getNetworkCount()); 397 assertEquals(2, cache.getHitCount()); 398 } 399 400 @Test public void redirectToCachedResult() throws Exception { 401 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("ABC")); 402 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 403 .addHeader("Location: /foo")); 404 server.enqueue(new MockResponse().setBody("DEF")); 405 server.play(); 406 407 assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo")))); 408 RecordedRequest request1 = server.takeRequest(); 409 assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); 410 assertEquals(0, request1.getSequenceNumber()); 411 412 assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar")))); 413 RecordedRequest request2 = server.takeRequest(); 414 assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); 415 assertEquals(1, request2.getSequenceNumber()); 416 417 // an unrelated request should reuse the pooled connection 418 assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz")))); 419 RecordedRequest request3 = server.takeRequest(); 420 assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); 421 assertEquals(2, request3.getSequenceNumber()); 422 } 423 424 @Test public void secureResponseCachingAndRedirects() throws IOException { 425 server.useHttps(sslContext.getSocketFactory(), false); 426 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 427 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 428 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 429 .addHeader("Location: /foo")); 430 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 431 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 432 .setBody("ABC")); 433 server.enqueue(new MockResponse().setBody("DEF")); 434 server.play(); 435 436 client.setSslSocketFactory(sslContext.getSocketFactory()); 437 client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 438 439 HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); 440 assertEquals("ABC", readAscii(connection1)); 441 assertNotNull(connection1.getCipherSuite()); 442 443 // Cached! 444 HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); 445 assertEquals("ABC", readAscii(connection2)); 446 assertNotNull(connection2.getCipherSuite()); 447 448 assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 449 assertEquals(2, cache.getHitCount()); 450 assertEquals(connection1.getCipherSuite(), connection2.getCipherSuite()); 451 } 452 453 /** 454 * We've had bugs where caching and cross-protocol redirects yield class 455 * cast exceptions internal to the cache because we incorrectly assumed that 456 * HttpsURLConnection was always HTTPS and HttpURLConnection was always HTTP; 457 * in practice redirects mean that each can do either. 458 * 459 * https://github.com/square/okhttp/issues/214 460 */ 461 @Test public void secureResponseCachingAndProtocolRedirects() throws IOException { 462 server2.useHttps(sslContext.getSocketFactory(), false); 463 server2.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 464 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 465 .setBody("ABC")); 466 server2.enqueue(new MockResponse().setBody("DEF")); 467 server2.play(); 468 469 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 470 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 471 .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) 472 .addHeader("Location: " + server2.getUrl("/"))); 473 server.play(); 474 475 client.setSslSocketFactory(sslContext.getSocketFactory()); 476 client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 477 478 HttpURLConnection connection1 = client.open(server.getUrl("/")); 479 assertEquals("ABC", readAscii(connection1)); 480 481 // Cached! 482 HttpURLConnection connection2 = client.open(server.getUrl("/")); 483 assertEquals("ABC", readAscii(connection2)); 484 485 assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 486 assertEquals(2, cache.getHitCount()); 487 } 488 489 @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException { 490 server.enqueue(new MockResponse().setBody("ABC")); 491 server.play(); 492 493 final AtomicReference<Request> requestRef = new AtomicReference<Request>(); 494 client.setOkResponseCache(new AbstractOkResponseCache() { 495 @Override public Response get(Request request) throws IOException { 496 requestRef.set(request); 497 return null; 498 } 499 }); 500 501 URL url = server.getUrl("/"); 502 URLConnection urlConnection = openConnection(url); 503 urlConnection.addRequestProperty("A", "android"); 504 readAscii(urlConnection); 505 assertEquals(Arrays.asList("android"), requestRef.get().headers("A")); 506 } 507 508 @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { 509 testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); 510 } 511 512 @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException { 513 testServerPrematureDisconnect(TransferKind.CHUNKED); 514 } 515 516 @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { 517 // Intentionally empty. This case doesn't make sense because there's no 518 // such thing as a premature disconnect when the disconnect itself 519 // indicates the end of the data stream. 520 } 521 522 private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { 523 MockResponse response = new MockResponse(); 524 transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); 525 server.enqueue(truncateViolently(response, 16)); 526 server.enqueue(new MockResponse().setBody("Request #2")); 527 server.play(); 528 529 BufferedReader reader = new BufferedReader( 530 new InputStreamReader(openConnection(server.getUrl("/")).getInputStream())); 531 assertEquals("ABCDE", reader.readLine()); 532 try { 533 reader.readLine(); 534 fail("This implementation silently ignored a truncated HTTP body."); 535 } catch (IOException expected) { 536 } finally { 537 reader.close(); 538 } 539 540 assertEquals(1, cache.getWriteAbortCount()); 541 assertEquals(0, cache.getWriteSuccessCount()); 542 URLConnection connection = openConnection(server.getUrl("/")); 543 assertEquals("Request #2", readAscii(connection)); 544 assertEquals(1, cache.getWriteAbortCount()); 545 assertEquals(1, cache.getWriteSuccessCount()); 546 } 547 548 @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException { 549 testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); 550 } 551 552 @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException { 553 testClientPrematureDisconnect(TransferKind.CHUNKED); 554 } 555 556 @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException { 557 testClientPrematureDisconnect(TransferKind.END_OF_STREAM); 558 } 559 560 private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { 561 // Setting a low transfer speed ensures that stream discarding will time out. 562 MockResponse response = new MockResponse().throttleBody(6, 1, TimeUnit.SECONDS); 563 transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); 564 server.enqueue(response); 565 server.enqueue(new MockResponse().setBody("Request #2")); 566 server.play(); 567 568 URLConnection connection = openConnection(server.getUrl("/")); 569 InputStream in = connection.getInputStream(); 570 assertEquals("ABCDE", readAscii(connection, 5)); 571 in.close(); 572 try { 573 in.read(); 574 fail("Expected an IOException because the stream is closed."); 575 } catch (IOException expected) { 576 } 577 578 assertEquals(1, cache.getWriteAbortCount()); 579 assertEquals(0, cache.getWriteSuccessCount()); 580 connection = openConnection(server.getUrl("/")); 581 assertEquals("Request #2", readAscii(connection)); 582 assertEquals(1, cache.getWriteAbortCount()); 583 assertEquals(1, cache.getWriteSuccessCount()); 584 } 585 586 @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { 587 // last modified: 105 seconds ago 588 // served: 5 seconds ago 589 // default lifetime: (105 - 5) / 10 = 10 seconds 590 // expires: 10 seconds from served date = 5 seconds from now 591 server.enqueue( 592 new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) 593 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) 594 .setBody("A")); 595 server.play(); 596 597 URL url = server.getUrl("/"); 598 assertEquals("A", readAscii(openConnection(url))); 599 URLConnection connection = openConnection(url); 600 assertEquals("A", readAscii(connection)); 601 assertNull(connection.getHeaderField("Warning")); 602 } 603 604 @Test public void defaultExpirationDateConditionallyCached() throws Exception { 605 // last modified: 115 seconds ago 606 // served: 15 seconds ago 607 // default lifetime: (115 - 15) / 10 = 10 seconds 608 // expires: 10 seconds from served date = 5 seconds ago 609 String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); 610 RecordedRequest conditionalRequest = assertConditionallyCached( 611 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 612 .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); 613 List<String> headers = conditionalRequest.getHeaders(); 614 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 615 } 616 617 @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { 618 // last modified: 105 days ago 619 // served: 5 days ago 620 // default lifetime: (105 - 5) / 10 = 10 days 621 // expires: 10 days from served date = 5 days from now 622 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) 623 .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) 624 .setBody("A")); 625 server.play(); 626 627 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 628 URLConnection connection = openConnection(server.getUrl("/")); 629 assertEquals("A", readAscii(connection)); 630 assertEquals("113 HttpURLConnection \"Heuristic expiration\"", 631 connection.getHeaderField("Warning")); 632 } 633 634 @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception { 635 server.enqueue( 636 new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) 637 .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) 638 .setBody("A")); 639 server.enqueue(new MockResponse().setBody("B")); 640 server.play(); 641 642 URL url = server.getUrl("/?foo=bar"); 643 assertEquals("A", readAscii(openConnection(url))); 644 assertEquals("B", readAscii(openConnection(url))); 645 } 646 647 @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { 648 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 649 RecordedRequest conditionalRequest = assertConditionallyCached( 650 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 651 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 652 List<String> headers = conditionalRequest.getHeaders(); 653 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 654 } 655 656 @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception { 657 assertNotCached(new MockResponse().addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 658 } 659 660 @Test public void expirationDateInTheFuture() throws Exception { 661 assertFullyCached(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 662 } 663 664 @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception { 665 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 666 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) 667 .addHeader("Cache-Control: max-age=60")); 668 } 669 670 @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { 671 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 672 RecordedRequest conditionalRequest = assertConditionallyCached( 673 new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) 674 .addHeader("Last-Modified: " + lastModifiedDate) 675 .addHeader("Cache-Control: max-age=60")); 676 List<String> headers = conditionalRequest.getHeaders(); 677 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 678 } 679 680 @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { 681 // Chrome interprets max-age relative to the local clock. Both our cache 682 // and Firefox both use the earlier of the local and server's clock. 683 assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) 684 .addHeader("Cache-Control: max-age=60")); 685 } 686 687 @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { 688 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 689 .addHeader("Cache-Control: max-age=60")); 690 } 691 692 @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { 693 assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60")); 694 } 695 696 @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { 697 assertFullyCached( 698 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 699 .addHeader("Cache-Control: max-age=60")); 700 } 701 702 @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { 703 assertFullyCached( 704 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 705 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 706 .addHeader("Cache-Control: max-age=60")); 707 } 708 709 @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception { 710 assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 711 .addHeader("Cache-Control: s-maxage=60") 712 .addHeader("Cache-Control: max-age=180")); 713 } 714 715 @Test public void maxAgePreferredOverHigherMaxAge() throws Exception { 716 assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 717 .addHeader("Cache-Control: s-maxage=180") 718 .addHeader("Cache-Control: max-age=60")); 719 } 720 721 @Test public void requestMethodOptionsIsNotCached() throws Exception { 722 testRequestMethod("OPTIONS", false); 723 } 724 725 @Test public void requestMethodGetIsCached() throws Exception { 726 testRequestMethod("GET", true); 727 } 728 729 @Test public void requestMethodHeadIsNotCached() throws Exception { 730 // We could support this but choose not to for implementation simplicity 731 testRequestMethod("HEAD", false); 732 } 733 734 @Test public void requestMethodPostIsNotCached() throws Exception { 735 // We could support this but choose not to for implementation simplicity 736 testRequestMethod("POST", false); 737 } 738 739 @Test public void requestMethodPutIsNotCached() throws Exception { 740 testRequestMethod("PUT", false); 741 } 742 743 @Test public void requestMethodDeleteIsNotCached() throws Exception { 744 testRequestMethod("DELETE", false); 745 } 746 747 @Test public void requestMethodTraceIsNotCached() throws Exception { 748 testRequestMethod("TRACE", false); 749 } 750 751 private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { 752 // 1. seed the cache (potentially) 753 // 2. expect a cache hit or miss 754 server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 755 .addHeader("X-Response-ID: 1")); 756 server.enqueue(new MockResponse().addHeader("X-Response-ID: 2")); 757 server.play(); 758 759 URL url = server.getUrl("/"); 760 761 HttpURLConnection request1 = openConnection(url); 762 request1.setRequestMethod(requestMethod); 763 addRequestBodyIfNecessary(requestMethod, request1); 764 assertEquals("1", request1.getHeaderField("X-Response-ID")); 765 766 URLConnection request2 = openConnection(url); 767 if (expectCached) { 768 assertEquals("1", request2.getHeaderField("X-Response-ID")); 769 } else { 770 assertEquals("2", request2.getHeaderField("X-Response-ID")); 771 } 772 } 773 774 @Test public void postInvalidatesCache() throws Exception { 775 testMethodInvalidates("POST"); 776 } 777 778 @Test public void putInvalidatesCache() throws Exception { 779 testMethodInvalidates("PUT"); 780 } 781 782 @Test public void deleteMethodInvalidatesCache() throws Exception { 783 testMethodInvalidates("DELETE"); 784 } 785 786 private void testMethodInvalidates(String requestMethod) throws Exception { 787 // 1. seed the cache 788 // 2. invalidate it 789 // 3. expect a cache miss 790 server.enqueue( 791 new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 792 server.enqueue(new MockResponse().setBody("B")); 793 server.enqueue(new MockResponse().setBody("C")); 794 server.play(); 795 796 URL url = server.getUrl("/"); 797 798 assertEquals("A", readAscii(openConnection(url))); 799 800 HttpURLConnection invalidate = openConnection(url); 801 invalidate.setRequestMethod(requestMethod); 802 addRequestBodyIfNecessary(requestMethod, invalidate); 803 assertEquals("B", readAscii(invalidate)); 804 805 assertEquals("C", readAscii(openConnection(url))); 806 } 807 808 @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception { 809 // 1. seed the cache 810 // 2. invalidate it with uncacheable response 811 // 3. expect a cache miss 812 server.enqueue( 813 new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 814 server.enqueue(new MockResponse().setBody("B").setResponseCode(500)); 815 server.enqueue(new MockResponse().setBody("C")); 816 server.play(); 817 818 URL url = server.getUrl("/"); 819 820 assertEquals("A", readAscii(openConnection(url))); 821 822 HttpURLConnection invalidate = openConnection(url); 823 invalidate.setRequestMethod("POST"); 824 addRequestBodyIfNecessary("POST", invalidate); 825 assertEquals("B", readAscii(invalidate)); 826 827 assertEquals("C", readAscii(openConnection(url))); 828 } 829 830 @Test public void etag() throws Exception { 831 RecordedRequest conditionalRequest = 832 assertConditionallyCached(new MockResponse().addHeader("ETag: v1")); 833 assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); 834 } 835 836 @Test public void etagAndExpirationDateInThePast() throws Exception { 837 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 838 RecordedRequest conditionalRequest = assertConditionallyCached( 839 new MockResponse().addHeader("ETag: v1") 840 .addHeader("Last-Modified: " + lastModifiedDate) 841 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 842 List<String> headers = conditionalRequest.getHeaders(); 843 assertTrue(headers.contains("If-None-Match: v1")); 844 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 845 } 846 847 @Test public void etagAndExpirationDateInTheFuture() throws Exception { 848 assertFullyCached(new MockResponse().addHeader("ETag: v1") 849 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 850 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 851 } 852 853 @Test public void cacheControlNoCache() throws Exception { 854 assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); 855 } 856 857 @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { 858 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 859 RecordedRequest conditionalRequest = assertConditionallyCached( 860 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 861 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 862 .addHeader("Cache-Control: no-cache")); 863 List<String> headers = conditionalRequest.getHeaders(); 864 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 865 } 866 867 @Test public void pragmaNoCache() throws Exception { 868 assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); 869 } 870 871 @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception { 872 String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); 873 RecordedRequest conditionalRequest = assertConditionallyCached( 874 new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 875 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 876 .addHeader("Pragma: no-cache")); 877 List<String> headers = conditionalRequest.getHeaders(); 878 assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); 879 } 880 881 @Test public void cacheControlNoStore() throws Exception { 882 assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); 883 } 884 885 @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { 886 assertNotCached(new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 887 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 888 .addHeader("Cache-Control: no-store")); 889 } 890 891 @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception { 892 // 1. request a range 893 // 2. request a full document, expecting a cache miss 894 server.enqueue(new MockResponse().setBody("AA") 895 .setResponseCode(HttpURLConnection.HTTP_PARTIAL) 896 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) 897 .addHeader("Content-Range: bytes 1000-1001/2000")); 898 server.enqueue(new MockResponse().setBody("BB")); 899 server.play(); 900 901 URL url = server.getUrl("/"); 902 903 URLConnection range = openConnection(url); 904 range.addRequestProperty("Range", "bytes=1000-1001"); 905 assertEquals("AA", readAscii(range)); 906 907 assertEquals("BB", readAscii(openConnection(url))); 908 } 909 910 @Test public void serverReturnsDocumentOlderThanCache() throws Exception { 911 server.enqueue(new MockResponse().setBody("A") 912 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 913 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 914 server.enqueue(new MockResponse().setBody("B") 915 .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); 916 server.play(); 917 918 URL url = server.getUrl("/"); 919 920 assertEquals("A", readAscii(openConnection(url))); 921 assertEquals("A", readAscii(openConnection(url))); 922 } 923 924 @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { 925 assertNonIdentityEncodingCached( 926 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 927 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 928 } 929 930 @Test public void nonIdentityEncodingAndFullCache() throws Exception { 931 assertNonIdentityEncodingCached( 932 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 933 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 934 } 935 936 private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { 937 server.enqueue( 938 response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip")); 939 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 940 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 941 942 server.play(); 943 944 // At least three request/response pairs are required because after the first request is cached 945 // a different execution path might be taken. Thus modifications to the cache applied during 946 // the second request might not be visible until another request is performed. 947 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 948 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 949 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 950 } 951 952 @Test public void notModifiedSpecifiesEncoding() throws Exception { 953 server.enqueue(new MockResponse() 954 .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) 955 .addHeader("Content-Encoding: gzip") 956 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 957 .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); 958 server.enqueue(new MockResponse() 959 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED) 960 .addHeader("Content-Encoding: gzip")); 961 server.enqueue(new MockResponse() 962 .setBody("DEFDEFDEF")); 963 964 server.play(); 965 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 966 assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); 967 assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/")))); 968 } 969 970 @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception { 971 server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A")); 972 server.enqueue(new MockResponse() 973 .clearHeaders() 974 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 975 server.play(); 976 977 ConnectionPool pool = ConnectionPool.getDefault(); 978 pool.evictAll(); 979 client.setConnectionPool(pool); 980 981 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 982 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 983 assertEquals(1, client.getConnectionPool().getConnectionCount()); 984 } 985 986 @Test public void expiresDateBeforeModifiedDate() throws Exception { 987 assertConditionallyCached( 988 new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 989 .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); 990 } 991 992 @Test public void requestMaxAge() throws IOException { 993 server.enqueue(new MockResponse().setBody("A") 994 .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) 995 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) 996 .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); 997 server.enqueue(new MockResponse().setBody("B")); 998 999 server.play(); 1000 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1001 1002 URLConnection connection = openConnection(server.getUrl("/")); 1003 connection.addRequestProperty("Cache-Control", "max-age=30"); 1004 assertEquals("B", readAscii(connection)); 1005 } 1006 1007 @Test public void requestMinFresh() throws IOException { 1008 server.enqueue(new MockResponse().setBody("A") 1009 .addHeader("Cache-Control: max-age=60") 1010 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1011 server.enqueue(new MockResponse().setBody("B")); 1012 1013 server.play(); 1014 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1015 1016 URLConnection connection = openConnection(server.getUrl("/")); 1017 connection.addRequestProperty("Cache-Control", "min-fresh=120"); 1018 assertEquals("B", readAscii(connection)); 1019 } 1020 1021 @Test public void requestMaxStale() throws IOException { 1022 server.enqueue(new MockResponse().setBody("A") 1023 .addHeader("Cache-Control: max-age=120") 1024 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); 1025 server.enqueue(new MockResponse().setBody("B")); 1026 1027 server.play(); 1028 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1029 1030 URLConnection connection = openConnection(server.getUrl("/")); 1031 connection.addRequestProperty("Cache-Control", "max-stale=180"); 1032 assertEquals("A", readAscii(connection)); 1033 assertEquals("110 HttpURLConnection \"Response is stale\"", 1034 connection.getHeaderField("Warning")); 1035 } 1036 1037 @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException { 1038 server.enqueue(new MockResponse().setBody("A") 1039 .addHeader("Cache-Control: max-age=120, must-revalidate") 1040 .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); 1041 server.enqueue(new MockResponse().setBody("B")); 1042 1043 server.play(); 1044 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1045 1046 URLConnection connection = openConnection(server.getUrl("/")); 1047 connection.addRequestProperty("Cache-Control", "max-stale=180"); 1048 assertEquals("B", readAscii(connection)); 1049 } 1050 1051 @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { 1052 // (no responses enqueued) 1053 server.play(); 1054 1055 HttpURLConnection connection = openConnection(server.getUrl("/")); 1056 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1057 assertGatewayTimeout(connection); 1058 assertEquals(1, cache.getRequestCount()); 1059 assertEquals(0, cache.getNetworkCount()); 1060 assertEquals(0, cache.getHitCount()); 1061 } 1062 1063 @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { 1064 server.enqueue(new MockResponse().setBody("A") 1065 .addHeader("Cache-Control: max-age=30") 1066 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1067 server.play(); 1068 1069 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1070 URLConnection connection = openConnection(server.getUrl("/")); 1071 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1072 assertEquals("A", readAscii(connection)); 1073 assertEquals(2, cache.getRequestCount()); 1074 assertEquals(1, cache.getNetworkCount()); 1075 assertEquals(1, cache.getHitCount()); 1076 } 1077 1078 @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { 1079 server.enqueue(new MockResponse().setBody("A") 1080 .addHeader("Cache-Control: max-age=30") 1081 .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); 1082 server.play(); 1083 1084 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1085 HttpURLConnection connection = openConnection(server.getUrl("/")); 1086 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1087 assertGatewayTimeout(connection); 1088 assertEquals(2, cache.getRequestCount()); 1089 assertEquals(1, cache.getNetworkCount()); 1090 assertEquals(0, cache.getHitCount()); 1091 } 1092 1093 @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { 1094 server.enqueue(new MockResponse().setBody("A")); 1095 server.play(); 1096 1097 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1098 HttpURLConnection connection = openConnection(server.getUrl("/")); 1099 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1100 assertGatewayTimeout(connection); 1101 assertEquals(2, cache.getRequestCount()); 1102 assertEquals(1, cache.getNetworkCount()); 1103 assertEquals(0, cache.getHitCount()); 1104 } 1105 1106 @Test public void requestCacheControlNoCache() throws Exception { 1107 server.enqueue( 1108 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 1109 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 1110 .addHeader("Cache-Control: max-age=60") 1111 .setBody("A")); 1112 server.enqueue(new MockResponse().setBody("B")); 1113 server.play(); 1114 1115 URL url = server.getUrl("/"); 1116 assertEquals("A", readAscii(openConnection(url))); 1117 URLConnection connection = openConnection(url); 1118 connection.setRequestProperty("Cache-Control", "no-cache"); 1119 assertEquals("B", readAscii(connection)); 1120 } 1121 1122 @Test public void requestPragmaNoCache() throws Exception { 1123 server.enqueue( 1124 new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) 1125 .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) 1126 .addHeader("Cache-Control: max-age=60") 1127 .setBody("A")); 1128 server.enqueue(new MockResponse().setBody("B")); 1129 server.play(); 1130 1131 URL url = server.getUrl("/"); 1132 assertEquals("A", readAscii(openConnection(url))); 1133 URLConnection connection = openConnection(url); 1134 connection.setRequestProperty("Pragma", "no-cache"); 1135 assertEquals("B", readAscii(connection)); 1136 } 1137 1138 @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception { 1139 MockResponse response = 1140 new MockResponse().addHeader("ETag: v3").addHeader("Cache-Control: max-age=0"); 1141 String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); 1142 RecordedRequest request = 1143 assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate); 1144 List<String> headers = request.getHeaders(); 1145 assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); 1146 assertFalse(headers.contains("If-None-Match: v3")); 1147 } 1148 1149 @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { 1150 String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); 1151 MockResponse response = new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) 1152 .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) 1153 .addHeader("Cache-Control: max-age=0"); 1154 RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1"); 1155 List<String> headers = request.getHeaders(); 1156 assertTrue(headers.contains("If-None-Match: v1")); 1157 assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); 1158 } 1159 1160 private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, 1161 String conditionValue) throws Exception { 1162 server.enqueue(seed.setBody("A")); 1163 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1164 server.play(); 1165 1166 URL url = server.getUrl("/"); 1167 assertEquals("A", readAscii(openConnection(url))); 1168 1169 HttpURLConnection connection = openConnection(url); 1170 connection.addRequestProperty(conditionName, conditionValue); 1171 assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); 1172 assertEquals("", readAscii(connection)); 1173 1174 server.takeRequest(); // seed 1175 return server.takeRequest(); 1176 } 1177 1178 /** 1179 * Confirm that {@link URLConnection#setIfModifiedSince} causes an 1180 * If-Modified-Since header with a GMT timestamp. 1181 * 1182 * https://code.google.com/p/android/issues/detail?id=66135 1183 */ 1184 @Test public void setIfModifiedSince() throws Exception { 1185 server.enqueue(new MockResponse().setBody("A")); 1186 server.play(); 1187 1188 URL url = server.getUrl("/"); 1189 URLConnection connection = openConnection(url); 1190 connection.setIfModifiedSince(1393666200000L); 1191 assertEquals("A", readAscii(connection)); 1192 RecordedRequest request = server.takeRequest(); 1193 String ifModifiedSinceHeader = request.getHeader("If-Modified-Since"); 1194 assertEquals("Sat, 01 Mar 2014 09:30:00 GMT", ifModifiedSinceHeader); 1195 } 1196 1197 /** 1198 * For Last-Modified and Date headers, we should echo the date back in the 1199 * exact format we were served. 1200 */ 1201 @Test public void retainServedDateFormat() throws Exception { 1202 // Serve a response with a non-standard date format that OkHttp supports. 1203 Date lastModifiedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1)); 1204 Date servedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2)); 1205 DateFormat dateFormat = new SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US); 1206 dateFormat.setTimeZone(TimeZone.getTimeZone("EDT")); 1207 String lastModifiedString = dateFormat.format(lastModifiedDate); 1208 String servedString = dateFormat.format(servedDate); 1209 1210 // This response should be conditionally cached. 1211 server.enqueue(new MockResponse() 1212 .addHeader("Last-Modified: " + lastModifiedString) 1213 .addHeader("Expires: " + servedString) 1214 .setBody("A")); 1215 server.enqueue(new MockResponse() 1216 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1217 server.play(); 1218 1219 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1220 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1221 1222 // The first request has no conditions. 1223 RecordedRequest request1 = server.takeRequest(); 1224 assertNull(request1.getHeader("If-Modified-Since")); 1225 1226 // The 2nd request uses the server's date format. 1227 RecordedRequest request2 = server.takeRequest(); 1228 assertEquals(lastModifiedString, request2.getHeader("If-Modified-Since")); 1229 } 1230 1231 @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { 1232 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1233 server.play(); 1234 1235 HttpURLConnection connection = openConnection(server.getUrl("/")); 1236 String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); 1237 connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); 1238 assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); 1239 assertEquals("", readAscii(connection)); 1240 } 1241 1242 @Test public void authorizationRequestHeaderPreventsCaching() throws Exception { 1243 server.enqueue( 1244 new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) 1245 .addHeader("Cache-Control: max-age=60") 1246 .setBody("A")); 1247 server.enqueue(new MockResponse().setBody("B")); 1248 server.play(); 1249 1250 URL url = server.getUrl("/"); 1251 URLConnection connection = openConnection(url); 1252 connection.addRequestProperty("Authorization", "password"); 1253 assertEquals("A", readAscii(connection)); 1254 assertEquals("B", readAscii(openConnection(url))); 1255 } 1256 1257 @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { 1258 assertAuthorizationRequestFullyCached( 1259 new MockResponse().addHeader("Cache-Control: s-maxage=60")); 1260 } 1261 1262 @Test public void authorizationResponseCachedWithPublic() throws Exception { 1263 assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public")); 1264 } 1265 1266 @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception { 1267 assertAuthorizationRequestFullyCached( 1268 new MockResponse().addHeader("Cache-Control: must-revalidate")); 1269 } 1270 1271 public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { 1272 server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A")); 1273 server.enqueue(new MockResponse().setBody("B")); 1274 server.play(); 1275 1276 URL url = server.getUrl("/"); 1277 URLConnection connection = openConnection(url); 1278 connection.addRequestProperty("Authorization", "password"); 1279 assertEquals("A", readAscii(connection)); 1280 assertEquals("A", readAscii(openConnection(url))); 1281 } 1282 1283 @Test public void contentLocationDoesNotPopulateCache() throws Exception { 1284 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1285 .addHeader("Content-Location: /bar") 1286 .setBody("A")); 1287 server.enqueue(new MockResponse().setBody("B")); 1288 server.play(); 1289 1290 assertEquals("A", readAscii(openConnection(server.getUrl("/foo")))); 1291 assertEquals("B", readAscii(openConnection(server.getUrl("/bar")))); 1292 } 1293 1294 @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { 1295 server.enqueue( 1296 new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); 1297 server.enqueue(new MockResponse().setBody("B")); 1298 server.play(); 1299 1300 URLConnection connection = openConnection(server.getUrl("/")); 1301 connection.setUseCaches(false); 1302 assertEquals("A", readAscii(connection)); 1303 assertEquals("B", readAscii(openConnection(server.getUrl("/")))); 1304 } 1305 1306 @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { 1307 server.enqueue( 1308 new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); 1309 server.enqueue(new MockResponse().setBody("B")); 1310 server.play(); 1311 1312 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1313 URLConnection connection = openConnection(server.getUrl("/")); 1314 connection.setUseCaches(false); 1315 assertEquals("B", readAscii(connection)); 1316 } 1317 1318 @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { 1319 URL url = new URL("http://localhost/"); 1320 URLConnection c1 = openConnection(url); 1321 URLConnection c2 = openConnection(url); 1322 assertTrue(c1.getDefaultUseCaches()); 1323 c1.setDefaultUseCaches(false); 1324 try { 1325 assertTrue(c1.getUseCaches()); 1326 assertTrue(c2.getUseCaches()); 1327 URLConnection c3 = openConnection(url); 1328 assertFalse(c3.getUseCaches()); 1329 } finally { 1330 c1.setDefaultUseCaches(true); 1331 } 1332 } 1333 1334 @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { 1335 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1336 .addHeader("Cache-Control: max-age=0") 1337 .setBody("A")); 1338 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1339 server.enqueue(new MockResponse().setBody("B")); 1340 server.play(); 1341 1342 assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); 1343 assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); 1344 assertEquals("B", readAscii(openConnection(server.getUrl("/b")))); 1345 1346 assertEquals(0, server.takeRequest().getSequenceNumber()); 1347 assertEquals(1, server.takeRequest().getSequenceNumber()); 1348 assertEquals(2, server.takeRequest().getSequenceNumber()); 1349 } 1350 1351 @Test public void statisticsConditionalCacheMiss() throws Exception { 1352 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1353 .addHeader("Cache-Control: max-age=0") 1354 .setBody("A")); 1355 server.enqueue(new MockResponse().setBody("B")); 1356 server.enqueue(new MockResponse().setBody("C")); 1357 server.play(); 1358 1359 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1360 assertEquals(1, cache.getRequestCount()); 1361 assertEquals(1, cache.getNetworkCount()); 1362 assertEquals(0, cache.getHitCount()); 1363 assertEquals("B", readAscii(openConnection(server.getUrl("/")))); 1364 assertEquals("C", readAscii(openConnection(server.getUrl("/")))); 1365 assertEquals(3, cache.getRequestCount()); 1366 assertEquals(3, cache.getNetworkCount()); 1367 assertEquals(0, cache.getHitCount()); 1368 } 1369 1370 @Test public void statisticsConditionalCacheHit() throws Exception { 1371 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1372 .addHeader("Cache-Control: max-age=0") 1373 .setBody("A")); 1374 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1375 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1376 server.play(); 1377 1378 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1379 assertEquals(1, cache.getRequestCount()); 1380 assertEquals(1, cache.getNetworkCount()); 1381 assertEquals(0, cache.getHitCount()); 1382 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1383 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1384 assertEquals(3, cache.getRequestCount()); 1385 assertEquals(3, cache.getNetworkCount()); 1386 assertEquals(2, cache.getHitCount()); 1387 } 1388 1389 @Test public void statisticsFullCacheHit() throws Exception { 1390 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A")); 1391 server.play(); 1392 1393 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1394 assertEquals(1, cache.getRequestCount()); 1395 assertEquals(1, cache.getNetworkCount()); 1396 assertEquals(0, cache.getHitCount()); 1397 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1398 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1399 assertEquals(3, cache.getRequestCount()); 1400 assertEquals(1, cache.getNetworkCount()); 1401 assertEquals(2, cache.getHitCount()); 1402 } 1403 1404 @Test public void varyMatchesChangedRequestHeaderField() throws Exception { 1405 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1406 .addHeader("Vary: Accept-Language") 1407 .setBody("A")); 1408 server.enqueue(new MockResponse().setBody("B")); 1409 server.play(); 1410 1411 URL url = server.getUrl("/"); 1412 HttpURLConnection frConnection = openConnection(url); 1413 frConnection.addRequestProperty("Accept-Language", "fr-CA"); 1414 assertEquals("A", readAscii(frConnection)); 1415 1416 HttpURLConnection enConnection = openConnection(url); 1417 enConnection.addRequestProperty("Accept-Language", "en-US"); 1418 assertEquals("B", readAscii(enConnection)); 1419 } 1420 1421 @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception { 1422 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1423 .addHeader("Vary: Accept-Language") 1424 .setBody("A")); 1425 server.enqueue(new MockResponse().setBody("B")); 1426 server.play(); 1427 1428 URL url = server.getUrl("/"); 1429 URLConnection connection1 = openConnection(url); 1430 connection1.addRequestProperty("Accept-Language", "fr-CA"); 1431 assertEquals("A", readAscii(connection1)); 1432 URLConnection connection2 = openConnection(url); 1433 connection2.addRequestProperty("Accept-Language", "fr-CA"); 1434 assertEquals("A", readAscii(connection2)); 1435 } 1436 1437 @Test public void varyMatchesAbsentRequestHeaderField() throws Exception { 1438 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1439 .addHeader("Vary: Foo") 1440 .setBody("A")); 1441 server.enqueue(new MockResponse().setBody("B")); 1442 server.play(); 1443 1444 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1445 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1446 } 1447 1448 @Test public void varyMatchesAddedRequestHeaderField() throws Exception { 1449 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1450 .addHeader("Vary: Foo") 1451 .setBody("A")); 1452 server.enqueue(new MockResponse().setBody("B")); 1453 server.play(); 1454 1455 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1456 URLConnection fooConnection = openConnection(server.getUrl("/")); 1457 fooConnection.addRequestProperty("Foo", "bar"); 1458 assertEquals("B", readAscii(fooConnection)); 1459 } 1460 1461 @Test public void varyMatchesRemovedRequestHeaderField() throws Exception { 1462 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1463 .addHeader("Vary: Foo") 1464 .setBody("A")); 1465 server.enqueue(new MockResponse().setBody("B")); 1466 server.play(); 1467 1468 URLConnection fooConnection = openConnection(server.getUrl("/")); 1469 fooConnection.addRequestProperty("Foo", "bar"); 1470 assertEquals("A", readAscii(fooConnection)); 1471 assertEquals("B", readAscii(openConnection(server.getUrl("/")))); 1472 } 1473 1474 @Test public void varyFieldsAreCaseInsensitive() throws Exception { 1475 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1476 .addHeader("Vary: ACCEPT-LANGUAGE") 1477 .setBody("A")); 1478 server.enqueue(new MockResponse().setBody("B")); 1479 server.play(); 1480 1481 URL url = server.getUrl("/"); 1482 URLConnection connection1 = openConnection(url); 1483 connection1.addRequestProperty("Accept-Language", "fr-CA"); 1484 assertEquals("A", readAscii(connection1)); 1485 URLConnection connection2 = openConnection(url); 1486 connection2.addRequestProperty("accept-language", "fr-CA"); 1487 assertEquals("A", readAscii(connection2)); 1488 } 1489 1490 @Test public void varyMultipleFieldsWithMatch() throws Exception { 1491 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1492 .addHeader("Vary: Accept-Language, Accept-Charset") 1493 .addHeader("Vary: Accept-Encoding") 1494 .setBody("A")); 1495 server.enqueue(new MockResponse().setBody("B")); 1496 server.play(); 1497 1498 URL url = server.getUrl("/"); 1499 URLConnection connection1 = openConnection(url); 1500 connection1.addRequestProperty("Accept-Language", "fr-CA"); 1501 connection1.addRequestProperty("Accept-Charset", "UTF-8"); 1502 connection1.addRequestProperty("Accept-Encoding", "identity"); 1503 assertEquals("A", readAscii(connection1)); 1504 URLConnection connection2 = openConnection(url); 1505 connection2.addRequestProperty("Accept-Language", "fr-CA"); 1506 connection2.addRequestProperty("Accept-Charset", "UTF-8"); 1507 connection2.addRequestProperty("Accept-Encoding", "identity"); 1508 assertEquals("A", readAscii(connection2)); 1509 } 1510 1511 @Test public void varyMultipleFieldsWithNoMatch() throws Exception { 1512 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1513 .addHeader("Vary: Accept-Language, Accept-Charset") 1514 .addHeader("Vary: Accept-Encoding") 1515 .setBody("A")); 1516 server.enqueue(new MockResponse().setBody("B")); 1517 server.play(); 1518 1519 URL url = server.getUrl("/"); 1520 URLConnection frConnection = openConnection(url); 1521 frConnection.addRequestProperty("Accept-Language", "fr-CA"); 1522 frConnection.addRequestProperty("Accept-Charset", "UTF-8"); 1523 frConnection.addRequestProperty("Accept-Encoding", "identity"); 1524 assertEquals("A", readAscii(frConnection)); 1525 URLConnection enConnection = openConnection(url); 1526 enConnection.addRequestProperty("Accept-Language", "en-CA"); 1527 enConnection.addRequestProperty("Accept-Charset", "UTF-8"); 1528 enConnection.addRequestProperty("Accept-Encoding", "identity"); 1529 assertEquals("B", readAscii(enConnection)); 1530 } 1531 1532 @Test public void varyMultipleFieldValuesWithMatch() throws Exception { 1533 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1534 .addHeader("Vary: Accept-Language") 1535 .setBody("A")); 1536 server.enqueue(new MockResponse().setBody("B")); 1537 server.play(); 1538 1539 URL url = server.getUrl("/"); 1540 URLConnection connection1 = openConnection(url); 1541 connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); 1542 connection1.addRequestProperty("Accept-Language", "en-US"); 1543 assertEquals("A", readAscii(connection1)); 1544 1545 URLConnection connection2 = openConnection(url); 1546 connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); 1547 connection2.addRequestProperty("Accept-Language", "en-US"); 1548 assertEquals("A", readAscii(connection2)); 1549 } 1550 1551 @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception { 1552 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1553 .addHeader("Vary: Accept-Language") 1554 .setBody("A")); 1555 server.enqueue(new MockResponse().setBody("B")); 1556 server.play(); 1557 1558 URL url = server.getUrl("/"); 1559 URLConnection connection1 = openConnection(url); 1560 connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); 1561 connection1.addRequestProperty("Accept-Language", "en-US"); 1562 assertEquals("A", readAscii(connection1)); 1563 1564 URLConnection connection2 = openConnection(url); 1565 connection2.addRequestProperty("Accept-Language", "fr-CA"); 1566 connection2.addRequestProperty("Accept-Language", "en-US"); 1567 assertEquals("B", readAscii(connection2)); 1568 } 1569 1570 @Test public void varyAsterisk() throws Exception { 1571 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1572 .addHeader("Vary: *") 1573 .setBody("A")); 1574 server.enqueue(new MockResponse().setBody("B")); 1575 server.play(); 1576 1577 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1578 assertEquals("B", readAscii(openConnection(server.getUrl("/")))); 1579 } 1580 1581 @Test public void varyAndHttps() throws Exception { 1582 server.useHttps(sslContext.getSocketFactory(), false); 1583 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") 1584 .addHeader("Vary: Accept-Language") 1585 .setBody("A")); 1586 server.enqueue(new MockResponse().setBody("B")); 1587 server.play(); 1588 1589 URL url = server.getUrl("/"); 1590 HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url); 1591 connection1.setSSLSocketFactory(sslContext.getSocketFactory()); 1592 connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 1593 connection1.addRequestProperty("Accept-Language", "en-US"); 1594 assertEquals("A", readAscii(connection1)); 1595 1596 HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url); 1597 connection2.setSSLSocketFactory(sslContext.getSocketFactory()); 1598 connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); 1599 connection2.addRequestProperty("Accept-Language", "en-US"); 1600 assertEquals("A", readAscii(connection2)); 1601 } 1602 1603 @Test public void cachePlusCookies() throws Exception { 1604 server.enqueue(new MockResponse().addHeader( 1605 "Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") 1606 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1607 .addHeader("Cache-Control: max-age=0") 1608 .setBody("A")); 1609 server.enqueue(new MockResponse().addHeader( 1610 "Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") 1611 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1612 server.play(); 1613 1614 URL url = server.getUrl("/"); 1615 assertEquals("A", readAscii(openConnection(url))); 1616 assertCookies(url, "a=FIRST"); 1617 assertEquals("A", readAscii(openConnection(url))); 1618 assertCookies(url, "a=SECOND"); 1619 } 1620 1621 @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception { 1622 server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD") 1623 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1624 .addHeader("Cache-Control: max-age=0") 1625 .setBody("A")); 1626 server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT") 1627 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1628 server.play(); 1629 1630 URLConnection connection1 = openConnection(server.getUrl("/")); 1631 assertEquals("A", readAscii(connection1)); 1632 assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); 1633 1634 URLConnection connection2 = openConnection(server.getUrl("/")); 1635 assertEquals("A", readAscii(connection2)); 1636 assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); 1637 } 1638 1639 @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception { 1640 server.enqueue(new MockResponse().addHeader("Transfer-Encoding: identity") 1641 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1642 .addHeader("Cache-Control: max-age=0") 1643 .setBody("A")); 1644 server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none") 1645 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1646 server.play(); 1647 1648 URLConnection connection1 = openConnection(server.getUrl("/")); 1649 assertEquals("A", readAscii(connection1)); 1650 assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); 1651 1652 URLConnection connection2 = openConnection(server.getUrl("/")); 1653 assertEquals("A", readAscii(connection2)); 1654 assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); 1655 } 1656 1657 @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception { 1658 server.enqueue(new MockResponse().addHeader("Warning: 199 test danger") 1659 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1660 .addHeader("Cache-Control: max-age=0") 1661 .setBody("A")); 1662 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1663 server.play(); 1664 1665 URLConnection connection1 = openConnection(server.getUrl("/")); 1666 assertEquals("A", readAscii(connection1)); 1667 assertEquals("199 test danger", connection1.getHeaderField("Warning")); 1668 1669 URLConnection connection2 = openConnection(server.getUrl("/")); 1670 assertEquals("A", readAscii(connection2)); 1671 assertEquals(null, connection2.getHeaderField("Warning")); 1672 } 1673 1674 @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception { 1675 server.enqueue(new MockResponse().addHeader("Warning: 299 test danger") 1676 .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) 1677 .addHeader("Cache-Control: max-age=0") 1678 .setBody("A")); 1679 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1680 server.play(); 1681 1682 URLConnection connection1 = openConnection(server.getUrl("/")); 1683 assertEquals("A", readAscii(connection1)); 1684 assertEquals("299 test danger", connection1.getHeaderField("Warning")); 1685 1686 URLConnection connection2 = openConnection(server.getUrl("/")); 1687 assertEquals("A", readAscii(connection2)); 1688 assertEquals("299 test danger", connection2.getHeaderField("Warning")); 1689 } 1690 1691 public void assertCookies(URL url, String... expectedCookies) throws Exception { 1692 List<String> actualCookies = new ArrayList<String>(); 1693 for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { 1694 actualCookies.add(cookie.toString()); 1695 } 1696 assertEquals(Arrays.asList(expectedCookies), actualCookies); 1697 } 1698 1699 @Test public void cachePlusRange() throws Exception { 1700 assertNotCached(new MockResponse().setResponseCode(HttpURLConnection.HTTP_PARTIAL) 1701 .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) 1702 .addHeader("Content-Range: bytes 100-100/200") 1703 .addHeader("Cache-Control: max-age=60")); 1704 } 1705 1706 @Test public void conditionalHitUpdatesCache() throws Exception { 1707 server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) 1708 .addHeader("Cache-Control: max-age=0") 1709 .setBody("A")); 1710 server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=30") 1711 .addHeader("Allow: GET, HEAD") 1712 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1713 server.enqueue(new MockResponse().setBody("B")); 1714 server.play(); 1715 1716 // cache miss; seed the cache 1717 HttpURLConnection connection1 = openConnection(server.getUrl("/a")); 1718 assertEquals("A", readAscii(connection1)); 1719 assertEquals(null, connection1.getHeaderField("Allow")); 1720 1721 // conditional cache hit; update the cache 1722 HttpURLConnection connection2 = openConnection(server.getUrl("/a")); 1723 assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); 1724 assertEquals("A", readAscii(connection2)); 1725 assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); 1726 1727 // full cache hit 1728 HttpURLConnection connection3 = openConnection(server.getUrl("/a")); 1729 assertEquals("A", readAscii(connection3)); 1730 assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); 1731 1732 assertEquals(2, server.getRequestCount()); 1733 } 1734 1735 @Test public void responseSourceHeaderCached() throws IOException { 1736 server.enqueue(new MockResponse().setBody("A") 1737 .addHeader("Cache-Control: max-age=30") 1738 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1739 server.play(); 1740 1741 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1742 URLConnection connection = openConnection(server.getUrl("/")); 1743 connection.addRequestProperty("Cache-Control", "only-if-cached"); 1744 assertEquals("A", readAscii(connection)); 1745 1746 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1747 assertEquals(ResponseSource.CACHE + " 200", source); 1748 } 1749 1750 @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException { 1751 server.enqueue(new MockResponse().setBody("A") 1752 .addHeader("Cache-Control: max-age=30") 1753 .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES))); 1754 server.enqueue(new MockResponse().setBody("B") 1755 .addHeader("Cache-Control: max-age=30") 1756 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1757 server.play(); 1758 1759 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1760 HttpURLConnection connection = openConnection(server.getUrl("/")); 1761 assertEquals("B", readAscii(connection)); 1762 1763 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1764 assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source); 1765 } 1766 1767 @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException { 1768 server.enqueue(new MockResponse().setBody("A") 1769 .addHeader("Cache-Control: max-age=0") 1770 .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); 1771 server.enqueue(new MockResponse().setResponseCode(304)); 1772 server.play(); 1773 1774 assertEquals("A", readAscii(openConnection(server.getUrl("/")))); 1775 HttpURLConnection connection = openConnection(server.getUrl("/")); 1776 assertEquals("A", readAscii(connection)); 1777 1778 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1779 assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source); 1780 } 1781 1782 @Test public void responseSourceHeaderFetched() throws IOException { 1783 server.enqueue(new MockResponse().setBody("A")); 1784 server.play(); 1785 1786 URLConnection connection = openConnection(server.getUrl("/")); 1787 assertEquals("A", readAscii(connection)); 1788 1789 String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE); 1790 assertEquals(ResponseSource.NETWORK + " 200", source); 1791 } 1792 1793 @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception { 1794 server.enqueue(new MockResponse() 1795 .addHeader("Cache-Control: max-age=120") 1796 .addHeader(": A") 1797 .setBody("body")); 1798 server.play(); 1799 HttpURLConnection connection = client.open(server.getUrl("/")); 1800 assertEquals("A", connection.getHeaderField("")); 1801 } 1802 1803 /** 1804 * Old implementations of OkHttp's response cache wrote header fields like 1805 * ":status: 200 OK". This broke our cached response parser because it split 1806 * on the first colon. This regression test exists to help us read these old 1807 * bad cache entries. 1808 * 1809 * https://github.com/square/okhttp/issues/227 1810 */ 1811 @Test public void testGoldenCacheResponse() throws Exception { 1812 cache.close(); 1813 server.enqueue(new MockResponse() 1814 .clearHeaders() 1815 .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1816 server.play(); 1817 1818 URL url = server.getUrl("/"); 1819 String urlKey = Util.hash(url.toString()); 1820 String entryMetadata = "" 1821 + "" + url + "\n" 1822 + "GET\n" 1823 + "0\n" 1824 + "HTTP/1.1 200 OK\n" 1825 + "7\n" 1826 + ":status: 200 OK\n" 1827 + ":version: HTTP/1.1\n" 1828 + "etag: foo\n" 1829 + "content-length: 3\n" 1830 + "OkHttp-Received-Millis: " + System.currentTimeMillis() + "\n" 1831 + "X-Android-Response-Source: NETWORK 200\n" 1832 + "OkHttp-Sent-Millis: " + System.currentTimeMillis() + "\n" 1833 + "\n" 1834 + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n" 1835 + "1\n" 1836 + "MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgy" 1837 + "OTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEF" 1838 + "AAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUc" 1839 + "ltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFK" 1840 + "v6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP" 1841 + "7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dm" 1842 + "yFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg==\n" 1843 + "-1\n"; 1844 String entryBody = "abc"; 1845 String journalBody = "" 1846 + "libcore.io.DiskLruCache\n" 1847 + "1\n" 1848 + "201105\n" 1849 + "2\n" 1850 + "\n" 1851 + "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n"; 1852 writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata); 1853 writeFile(cache.getDirectory(), urlKey + ".1", entryBody); 1854 writeFile(cache.getDirectory(), "journal", journalBody); 1855 cache = new HttpResponseCache(cache.getDirectory(), Integer.MAX_VALUE); 1856 client.setOkResponseCache(cache); 1857 1858 HttpURLConnection connection = client.open(url); 1859 assertEquals(entryBody, readAscii(connection)); 1860 assertEquals("3", connection.getHeaderField("Content-Length")); 1861 assertEquals("foo", connection.getHeaderField("etag")); 1862 } 1863 1864 private void writeFile(File directory, String file, String content) throws IOException { 1865 OutputStream out = new FileOutputStream(new File(directory, file)); 1866 out.write(content.getBytes(Util.UTF_8)); 1867 out.close(); 1868 } 1869 1870 /** 1871 * @param delta the offset from the current date to use. Negative 1872 * values yield dates in the past; positive values yield dates in the 1873 * future. 1874 */ 1875 private String formatDate(long delta, TimeUnit timeUnit) { 1876 return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); 1877 } 1878 1879 private String formatDate(Date date) { 1880 DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); 1881 rfc1123.setTimeZone(TimeZone.getTimeZone("GMT")); 1882 return rfc1123.format(date); 1883 } 1884 1885 private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) 1886 throws IOException { 1887 if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { 1888 invalidate.setDoOutput(true); 1889 OutputStream requestBody = invalidate.getOutputStream(); 1890 requestBody.write('x'); 1891 requestBody.close(); 1892 } 1893 } 1894 1895 private void assertNotCached(MockResponse response) throws Exception { 1896 server.enqueue(response.setBody("A")); 1897 server.enqueue(new MockResponse().setBody("B")); 1898 server.play(); 1899 1900 URL url = server.getUrl("/"); 1901 assertEquals("A", readAscii(openConnection(url))); 1902 assertEquals("B", readAscii(openConnection(url))); 1903 } 1904 1905 /** @return the request with the conditional get headers. */ 1906 private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { 1907 // scenario 1: condition succeeds 1908 server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); 1909 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 1910 1911 // scenario 2: condition fails 1912 server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); 1913 server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); 1914 1915 server.play(); 1916 1917 URL valid = server.getUrl("/valid"); 1918 HttpURLConnection connection1 = openConnection(valid); 1919 assertEquals("A", readAscii(connection1)); 1920 assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); 1921 assertEquals("A-OK", connection1.getResponseMessage()); 1922 HttpURLConnection connection2 = openConnection(valid); 1923 assertEquals("A", readAscii(connection2)); 1924 assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); 1925 assertEquals("A-OK", connection2.getResponseMessage()); 1926 1927 URL invalid = server.getUrl("/invalid"); 1928 HttpURLConnection connection3 = openConnection(invalid); 1929 assertEquals("B", readAscii(connection3)); 1930 assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); 1931 assertEquals("B-OK", connection3.getResponseMessage()); 1932 HttpURLConnection connection4 = openConnection(invalid); 1933 assertEquals("C", readAscii(connection4)); 1934 assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); 1935 assertEquals("C-OK", connection4.getResponseMessage()); 1936 1937 server.takeRequest(); // regular get 1938 return server.takeRequest(); // conditional get 1939 } 1940 1941 private void assertFullyCached(MockResponse response) throws Exception { 1942 server.enqueue(response.setBody("A")); 1943 server.enqueue(response.setBody("B")); 1944 server.play(); 1945 1946 URL url = server.getUrl("/"); 1947 assertEquals("A", readAscii(openConnection(url))); 1948 assertEquals("A", readAscii(openConnection(url))); 1949 } 1950 1951 /** 1952 * Shortens the body of {@code response} but not the corresponding headers. 1953 * Only useful to test how clients respond to the premature conclusion of 1954 * the HTTP body. 1955 */ 1956 private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { 1957 response.setSocketPolicy(DISCONNECT_AT_END); 1958 List<String> headers = new ArrayList<String>(response.getHeaders()); 1959 response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); 1960 response.getHeaders().clear(); 1961 response.getHeaders().addAll(headers); 1962 return response; 1963 } 1964 1965 /** 1966 * Reads {@code count} characters from the stream. If the stream is 1967 * exhausted before {@code count} characters can be read, the remaining 1968 * characters are returned and the stream is closed. 1969 */ 1970 private String readAscii(URLConnection connection, int count) throws IOException { 1971 HttpURLConnection httpConnection = (HttpURLConnection) connection; 1972 InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST 1973 ? connection.getInputStream() 1974 : httpConnection.getErrorStream(); 1975 StringBuilder result = new StringBuilder(); 1976 for (int i = 0; i < count; i++) { 1977 int value = in.read(); 1978 if (value == -1) { 1979 in.close(); 1980 break; 1981 } 1982 result.append((char) value); 1983 } 1984 return result.toString(); 1985 } 1986 1987 private String readAscii(URLConnection connection) throws IOException { 1988 return readAscii(connection, Integer.MAX_VALUE); 1989 } 1990 1991 private void reliableSkip(InputStream in, int length) throws IOException { 1992 while (length > 0) { 1993 length -= in.skip(length); 1994 } 1995 } 1996 1997 private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { 1998 try { 1999 connection.getInputStream(); 2000 fail(); 2001 } catch (FileNotFoundException expected) { 2002 } 2003 assertEquals(504, connection.getResponseCode()); 2004 assertEquals(-1, connection.getErrorStream().read()); 2005 assertEquals(ResponseSource.NONE + " 504", 2006 connection.getHeaderField(OkHeaders.RESPONSE_SOURCE)); 2007 } 2008 2009 enum TransferKind { 2010 CHUNKED() { 2011 @Override void setBody(MockResponse response, byte[] content, int chunkSize) 2012 throws IOException { 2013 response.setChunkedBody(content, chunkSize); 2014 } 2015 }, 2016 FIXED_LENGTH() { 2017 @Override void setBody(MockResponse response, byte[] content, int chunkSize) { 2018 response.setBody(content); 2019 } 2020 }, 2021 END_OF_STREAM() { 2022 @Override void setBody(MockResponse response, byte[] content, int chunkSize) { 2023 response.setBody(content); 2024 response.setSocketPolicy(DISCONNECT_AT_END); 2025 for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) { 2026 if (h.next().startsWith("Content-Length:")) { 2027 h.remove(); 2028 break; 2029 } 2030 } 2031 } 2032 }; 2033 2034 abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; 2035 2036 void setBody(MockResponse response, String content, int chunkSize) throws IOException { 2037 setBody(response, content.getBytes("UTF-8"), chunkSize); 2038 } 2039 } 2040 2041 private <T> List<T> toListOrNull(T[] arrayOrNull) { 2042 return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; 2043 } 2044 2045 /** Returns a gzipped copy of {@code bytes}. */ 2046 public byte[] gzip(byte[] bytes) throws IOException { 2047 ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); 2048 OutputStream gzippedOut = new GZIPOutputStream(bytesOut); 2049 gzippedOut.write(bytes); 2050 gzippedOut.close(); 2051 return bytesOut.toByteArray(); 2052 } 2053 2054 static abstract class AbstractOkResponseCache implements OkResponseCache { 2055 @Override public Response get(Request request) throws IOException { 2056 return null; 2057 } 2058 2059 @Override public CacheRequest put(Response response) throws IOException { 2060 return null; 2061 } 2062 2063 @Override public boolean maybeRemove(Request request) throws IOException { 2064 return false; 2065 } 2066 2067 @Override public void update(Response cached, Response network) throws IOException { 2068 } 2069 2070 @Override public void trackConditionalCacheHit() { 2071 } 2072 2073 @Override public void trackResponse(ResponseSource source) { 2074 } 2075 } 2076} 2077