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