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