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