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