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