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