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