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