Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
jvmTest.okhttp3.CacheTest Maven / Gradle / Ivy
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import javax.net.ssl.HostnameVerifier;
import java.io.IOException;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.ResponseCache;
import java.security.Principal;
import java.security.cert.Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.TimeZone;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import okhttp3.internal.Internal;
import okhttp3.internal.platform.Platform;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.FileSystem;
import okio.ForwardingFileSystem;
import okio.GzipSink;
import okio.Okio;
import okio.Path;
import okio.fakefilesystem.FakeFileSystem;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static mockwebserver3.SocketPolicy.DISCONNECT_AT_END;
import static okhttp3.internal.Internal.cacheGet;
import static okhttp3.tls.internal.TlsUtil.localhost;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.junit.jupiter.api.Assertions.fail;
@Tag("Slow")
public final class CacheTest {
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = (name, session) -> true;
public final FakeFileSystem fileSystem = new FakeFileSystem();
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
@RegisterExtension public final PlatformRule platform = new PlatformRule();
private MockWebServer server;
private MockWebServer server2;
private final HandshakeCertificates handshakeCertificates = localhost();
private OkHttpClient client;
private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@BeforeEach public void setUp(MockWebServer server, MockWebServer server2) throws Exception {
this.server = server;
this.server2 = server2;
platform.assumeNotOpenJSSE();
platform.assumeNotBouncyCastle();
server.setProtocolNegotiationEnabled(false);
fileSystem.emulateUnix();
cache = new Cache(Path.get("/cache/"), Integer.MAX_VALUE, fileSystem);
client = clientTestRule.newClientBuilder()
.cache(cache)
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();
}
@AfterEach public void tearDown() throws Exception {
ResponseCache.setDefault(null);
if (cache != null) {
cache.delete();
}
}
/**
* Test that response caching is consistent with the RI and the spec.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
*/
@Test public void responseCachingByResponseCode() throws Exception {
// Test each documented HTTP/1.1 code, plus the first unused value in each range.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// We can't test 100 because it's not really a response.
// assertCached(false, 100);
assertCached(false, 101);
assertCached(false, 102);
assertCached(true, 200);
assertCached(false, 201);
assertCached(false, 202);
assertCached(true, 203);
assertCached(true, 204);
assertCached(false, 205);
assertCached(false, 206); //Electing to not cache partial responses
assertCached(false, 207);
assertCached(true, 300);
assertCached(true, 301);
assertCached(true, 302);
assertCached(false, 303);
assertCached(false, 304);
assertCached(false, 305);
assertCached(false, 306);
assertCached(true, 307);
assertCached(true, 308);
assertCached(false, 400);
assertCached(false, 401);
assertCached(false, 402);
assertCached(false, 403);
assertCached(true, 404);
assertCached(true, 405);
assertCached(false, 406);
assertCached(false, 408);
assertCached(false, 409);
// the HTTP spec permits caching 410s, but the RI doesn't.
assertCached(true, 410);
assertCached(false, 411);
assertCached(false, 412);
assertCached(false, 413);
assertCached(true, 414);
assertCached(false, 415);
assertCached(false, 416);
assertCached(false, 417);
assertCached(false, 418);
assertCached(false, 500);
assertCached(true, 501);
assertCached(false, 502);
assertCached(false, 503);
assertCached(false, 504);
assertCached(false, 505);
assertCached(false, 506);
}
private void assertCached(boolean shouldPut, int responseCode) throws Exception {
int expectedResponseCode = responseCode;
server = new MockWebServer();
MockResponse mockResponse = new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(responseCode)
.setBody("ABCDE")
.addHeader("WWW-Authenticate: challenge");
if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) {
mockResponse.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"");
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
mockResponse.addHeader("WWW-Authenticate: Basic realm=\"protected area\"");
} else if (responseCode == HttpURLConnection.HTTP_NO_CONTENT
|| responseCode == HttpURLConnection.HTTP_RESET) {
mockResponse.setBody(""); // We forbid bodies for 204 and 205.
}
server.enqueue(mockResponse);
if (responseCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT) {
// 408's are a bit of an outlier because we may repeat the request if we encounter this
// response code. In this scenario, there are 2 responses: the initial 408 and then the 200
// because of the retry. We just want to ensure the initial 408 isn't cached.
expectedResponseCode = 200;
server.enqueue(new MockResponse()
.setHeader("Cache-Control", "no-store")
.setBody("FGHIJ"));
}
server.start();
Request request = new Request.Builder()
.url(server.url("/"))
.build();
Response response = client.newCall(request).execute();
assertThat(response.code()).isEqualTo(expectedResponseCode);
// Exhaust the content stream.
response.body().string();
Response cached = cacheGet(cache, request);
if (shouldPut) {
assertThat(cached).isNotNull();
cached.body().close();
} else {
assertThat(cached).isNull();
}
server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
}
@Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
testResponseCaching(TransferKind.FIXED_LENGTH);
}
@Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
testResponseCaching(TransferKind.CHUNKED);
}
@Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
testResponseCaching(TransferKind.END_OF_STREAM);
}
/**
* Skipping bytes in the input stream caused ResponseCache corruption.
* http://code.google.com/p/android/issues/detail?id=8175
*/
private void testResponseCaching(TransferKind transferKind) throws IOException {
MockResponse mockResponse = new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setStatus("HTTP/1.1 200 Fantastic");
transferKind.setBody(mockResponse, "I love puppies but hate spiders", 1);
server.enqueue(mockResponse);
// Make sure that calling skip() doesn't omit bytes from the cache.
Request request = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request).execute();
BufferedSource in1 = response1.body().source();
assertThat(in1.readUtf8("I love ".length())).isEqualTo("I love ");
in1.skip("puppies but hate ".length());
assertThat(in1.readUtf8("spiders".length())).isEqualTo("spiders");
assertThat(in1.exhausted()).isTrue();
in1.close();
assertThat(cache.writeSuccessCount()).isEqualTo(1);
assertThat(cache.writeAbortCount()).isEqualTo(0);
Response response2 = client.newCall(request).execute();
BufferedSource in2 = response2.body().source();
assertThat(in2.readUtf8("I love puppies but hate spiders".length())).isEqualTo(
"I love puppies but hate spiders");
assertThat(response2.code()).isEqualTo(200);
assertThat(response2.message()).isEqualTo("Fantastic");
assertThat(in2.exhausted()).isTrue();
in2.close();
assertThat(cache.writeSuccessCount()).isEqualTo(1);
assertThat(cache.writeAbortCount()).isEqualTo(0);
assertThat(cache.requestCount()).isEqualTo(2);
assertThat(cache.hitCount()).isEqualTo(1);
}
@Test public void secureResponseCaching() throws IOException {
server.useHttps(handshakeCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
client = client.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build();
Request request = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request).execute();
BufferedSource in = response1.body().source();
assertThat(in.readUtf8()).isEqualTo("ABC");
// OpenJDK 6 fails on this line, complaining that the connection isn't open yet
CipherSuite cipherSuite = response1.handshake().cipherSuite();
List localCerts = response1.handshake().localCertificates();
List serverCerts = response1.handshake().peerCertificates();
Principal peerPrincipal = response1.handshake().peerPrincipal();
Principal localPrincipal = response1.handshake().localPrincipal();
Response response2 = client.newCall(request).execute(); // Cached!
assertThat(response2.body().string()).isEqualTo("ABC");
assertThat(cache.requestCount()).isEqualTo(2);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(1);
assertThat(response2.handshake().cipherSuite()).isEqualTo(cipherSuite);
assertThat(response2.handshake().localCertificates()).isEqualTo(localCerts);
assertThat(response2.handshake().peerCertificates()).isEqualTo(serverCerts);
assertThat(response2.handshake().peerPrincipal()).isEqualTo(peerPrincipal);
assertThat(response2.handshake().localPrincipal()).isEqualTo(localPrincipal);
}
@Test public void responseCachingAndRedirects() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse()
.setBody("DEF"));
Request request = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request).execute();
assertThat(response1.body().string()).isEqualTo("ABC");
Response response2 = client.newCall(request).execute(); // Cached!
assertThat(response2.body().string()).isEqualTo("ABC");
// 2 requests + 2 redirects
assertThat(cache.requestCount()).isEqualTo(4);
assertThat(cache.networkCount()).isEqualTo(2);
assertThat(cache.hitCount()).isEqualTo(2);
}
@Test public void redirectToCachedResult() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("ABC"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse()
.setBody("DEF"));
Request request1 = new Request.Builder().url(server.url("/foo")).build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("ABC");
RecordedRequest recordedRequest1 = server.takeRequest();
assertThat(recordedRequest1.getRequestLine()).isEqualTo("GET /foo HTTP/1.1");
assertThat(recordedRequest1.getSequenceNumber()).isEqualTo(0);
Request request2 = new Request.Builder().url(server.url("/bar")).build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("ABC");
RecordedRequest recordedRequest2 = server.takeRequest();
assertThat(recordedRequest2.getRequestLine()).isEqualTo("GET /bar HTTP/1.1");
assertThat(recordedRequest2.getSequenceNumber()).isEqualTo(1);
// an unrelated request should reuse the pooled connection
Request request3 = new Request.Builder().url(server.url("/baz")).build();
Response response3 = client.newCall(request3).execute();
assertThat(response3.body().string()).isEqualTo("DEF");
RecordedRequest recordedRequest3 = server.takeRequest();
assertThat(recordedRequest3.getRequestLine()).isEqualTo("GET /baz HTTP/1.1");
assertThat(recordedRequest3.getSequenceNumber()).isEqualTo(2);
}
@Test public void secureResponseCachingAndRedirects() throws IOException {
server.useHttps(handshakeCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse()
.setBody("DEF"));
client = client.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build();
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("ABC");
assertThat(response1.handshake().cipherSuite()).isNotNull();
// Cached!
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("ABC");
assertThat(response2.handshake().cipherSuite()).isNotNull();
// 2 direct + 2 redirect = 4
assertThat(cache.requestCount()).isEqualTo(4);
assertThat(cache.hitCount()).isEqualTo(2);
assertThat(response2.handshake().cipherSuite()).isEqualTo(
response1.handshake().cipherSuite());
}
/**
* We've had bugs where caching and cross-protocol redirects yield class cast exceptions internal
* to the cache because we incorrectly assumed that HttpsURLConnection was always HTTPS and
* HttpURLConnection was always HTTP; in practice redirects mean that each can do either.
*
* https://github.com/square/okhttp/issues/214
*/
@Test public void secureResponseCachingAndProtocolRedirects() throws IOException {
server2.useHttps(handshakeCertificates.sslSocketFactory(), false);
server2.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server2.enqueue(new MockResponse()
.setBody("DEF"));
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: " + server2.url("/")));
client = client.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build();
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("ABC");
// Cached!
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("ABC");
// 2 direct + 2 redirect = 4
assertThat(cache.requestCount()).isEqualTo(4);
assertThat(cache.hitCount()).isEqualTo(2);
}
@Test public void foundCachedWithExpiresHeader() throws Exception {
temporaryRedirectCachedWithCachingHeader(302, "Expires", formatDate(1, TimeUnit.HOURS));
}
@Test public void foundCachedWithCacheControlHeader() throws Exception {
temporaryRedirectCachedWithCachingHeader(302, "Cache-Control", "max-age=60");
}
@Test public void temporaryRedirectCachedWithExpiresHeader() throws Exception {
temporaryRedirectCachedWithCachingHeader(307, "Expires", formatDate(1, TimeUnit.HOURS));
}
@Test public void temporaryRedirectCachedWithCacheControlHeader() throws Exception {
temporaryRedirectCachedWithCachingHeader(307, "Cache-Control", "max-age=60");
}
@Test public void foundNotCachedWithoutCacheHeader() throws Exception {
temporaryRedirectNotCachedWithoutCachingHeader(302);
}
@Test public void temporaryRedirectNotCachedWithoutCacheHeader() throws Exception {
temporaryRedirectNotCachedWithoutCachingHeader(307);
}
private void temporaryRedirectCachedWithCachingHeader(
int responseCode, String headerName, String headerValue) throws Exception {
server.enqueue(new MockResponse()
.setResponseCode(responseCode)
.addHeader(headerName, headerValue)
.addHeader("Location", "/a"));
server.enqueue(new MockResponse()
.addHeader(headerName, headerValue)
.setBody("a"));
server.enqueue(new MockResponse()
.setBody("b"));
server.enqueue(new MockResponse()
.setBody("c"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("a");
assertThat(get(url).body().string()).isEqualTo("a");
}
private void temporaryRedirectNotCachedWithoutCachingHeader(int responseCode) throws Exception {
server.enqueue(new MockResponse()
.setResponseCode(responseCode)
.addHeader("Location", "/a"));
server.enqueue(new MockResponse()
.setBody("a"));
server.enqueue(new MockResponse()
.setBody("b"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("a");
assertThat(get(url).body().string()).isEqualTo("b");
}
/** https://github.com/square/okhttp/issues/2198 */
@Test public void cachedRedirect() throws IOException {
server.enqueue(new MockResponse()
.setResponseCode(301)
.addHeader("Cache-Control: max-age=60")
.addHeader("Location: /bar"));
server.enqueue(new MockResponse()
.setBody("ABC"));
server.enqueue(new MockResponse()
.setBody("ABC"));
Request request1 = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("ABC");
Request request2 = new Request.Builder().url(server.url("/")).build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("ABC");
}
@Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
@Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
testServerPrematureDisconnect(TransferKind.CHUNKED);
}
@Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
// Intentionally empty. This case doesn't make sense because there's no
// such thing as a premature disconnect when the disconnect itself
// indicates the end of the data stream.
}
private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
MockResponse mockResponse = new MockResponse();
transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
server.enqueue(truncateViolently(mockResponse, 16));
server.enqueue(new MockResponse()
.setBody("Request #2"));
BufferedSource bodySource = get(server.url("/")).body().source();
assertThat(bodySource.readUtf8Line()).isEqualTo("ABCDE");
try {
bodySource.readUtf8(21);
fail("This implementation silently ignored a truncated HTTP body.");
} catch (IOException expected) {
} finally {
bodySource.close();
}
assertThat(cache.writeAbortCount()).isEqualTo(1);
assertThat(cache.writeSuccessCount()).isEqualTo(0);
Response response = get(server.url("/"));
assertThat(response.body().string()).isEqualTo("Request #2");
assertThat(cache.writeAbortCount()).isEqualTo(1);
assertThat(cache.writeSuccessCount()).isEqualTo(1);
}
@Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException {
testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
@Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException {
testClientPrematureDisconnect(TransferKind.CHUNKED);
}
@Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException {
testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
}
private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
// Setting a low transfer speed ensures that stream discarding will time out.
MockResponse mockResponse = new MockResponse()
.throttleBody(6, 1, TimeUnit.SECONDS);
transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(mockResponse);
server.enqueue(new MockResponse()
.setBody("Request #2"));
Response response1 = get(server.url("/"));
BufferedSource in = response1.body().source();
assertThat(in.readUtf8(5)).isEqualTo("ABCDE");
in.close();
try {
in.readByte();
fail("Expected an IllegalStateException because the source is closed.");
} catch (IllegalStateException expected) {
}
assertThat(cache.writeAbortCount()).isEqualTo(1);
assertThat(cache.writeSuccessCount()).isEqualTo(0);
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("Request #2");
assertThat(cache.writeAbortCount()).isEqualTo(1);
assertThat(cache.writeSuccessCount()).isEqualTo(1);
}
@Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception {
// last modified: 105 seconds ago
// served: 5 seconds ago
// default lifetime: (105 - 5) / 10 = 10 seconds
// expires: 10 seconds from served date = 5 seconds from now
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
HttpUrl url = server.url("/");
Response response1 = get(url);
assertThat(response1.body().string()).isEqualTo("A");
Response response2 = get(url);
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Warning")).isNull();
}
@Test public void defaultExpirationDateConditionallyCached() throws Exception {
// last modified: 115 seconds ago
// served: 15 seconds ago
// default lifetime: (115 - 15) / 10 = 10 seconds
// expires: 10 seconds from served date = 5 seconds ago
String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
// last modified: 105 days ago
// served: 5 days ago
// default lifetime: (105 - 5) / 10 = 10 days
// expires: 10 days from served date = 5 days from now
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
.addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
.setBody("A"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Response response = get(server.url("/"));
assertThat(response.body().string()).isEqualTo("A");
assertThat(response.header("Warning")).isEqualTo(
"113 HttpURLConnection \"Heuristic expiration\"");
}
@Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/").newBuilder().addQueryParameter("foo", "bar").build();
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("B");
}
@Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception {
String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
}
@Test public void expirationDateInTheFuture() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
}
@Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception {
String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Cache-Control: max-age=60"));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
// Chrome interprets max-age relative to the local clock. Both our cache
// and Firefox both use the earlier of the local and server's clock.
assertNotCached(new MockResponse()
.addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgeInTheFutureWithDateHeader() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=60"));
}
@Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
.addHeader("Cache-Control: s-maxage=60")
.addHeader("Cache-Control: max-age=180"));
}
@Test public void maxAgePreferredOverHigherMaxAge() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
.addHeader("Cache-Control: s-maxage=180")
.addHeader("Cache-Control: max-age=60"));
}
@Test public void requestMethodOptionsIsNotCached() throws Exception {
testRequestMethod("OPTIONS", false);
}
@Test public void requestMethodGetIsCached() throws Exception {
testRequestMethod("GET", true);
}
@Test public void requestMethodHeadIsNotCached() throws Exception {
// We could support this but choose not to for implementation simplicity
testRequestMethod("HEAD", false);
}
@Test public void requestMethodPostIsNotCached() throws Exception {
// We could support this but choose not to for implementation simplicity
testRequestMethod("POST", false);
}
@Test public void requestMethodPutIsNotCached() throws Exception {
testRequestMethod("PUT", false);
}
@Test public void requestMethodDeleteIsNotCached() throws Exception {
testRequestMethod("DELETE", false);
}
@Test public void requestMethodTraceIsNotCached() throws Exception {
testRequestMethod("TRACE", false);
}
private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception {
// 1. Seed the cache (potentially).
// 2. Expect a cache hit or miss.
server.enqueue(new MockResponse()
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("X-Response-ID: 1"));
server.enqueue(new MockResponse()
.addHeader("X-Response-ID: 2"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.method(requestMethod, requestBodyOrNull(requestMethod))
.build();
Response response1 = client.newCall(request).execute();
response1.body().close();
assertThat(response1.header("X-Response-ID")).isEqualTo("1");
Response response2 = get(url);
response2.body().close();
if (expectCached) {
assertThat(response2.header("X-Response-ID")).isEqualTo("1");
} else {
assertThat(response2.header("X-Response-ID")).isEqualTo("2");
}
}
private RequestBody requestBodyOrNull(String requestMethod) {
return (requestMethod.equals("POST") || requestMethod.equals("PUT"))
? RequestBody.create("foo", MediaType.get("text/plain"))
: null;
}
@Test public void postInvalidatesCache() throws Exception {
testMethodInvalidates("POST");
}
@Test public void putInvalidatesCache() throws Exception {
testMethodInvalidates("PUT");
}
@Test public void deleteMethodInvalidatesCache() throws Exception {
testMethodInvalidates("DELETE");
}
private void testMethodInvalidates(String requestMethod) throws Exception {
// 1. Seed the cache.
// 2. Invalidate it.
// 3. Expect a cache miss.
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setBody("B"));
server.enqueue(new MockResponse()
.setBody("C"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.method(requestMethod, requestBodyOrNull(requestMethod))
.build();
Response invalidate = client.newCall(request).execute();
assertThat(invalidate.body().string()).isEqualTo("B");
assertThat(get(url).body().string()).isEqualTo("C");
}
@Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
// 1. Seed the cache.
// 2. Invalidate it with an uncacheable response.
// 3. Expect a cache miss.
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setBody("B")
.setResponseCode(500));
server.enqueue(new MockResponse()
.setBody("C"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.method("POST", requestBodyOrNull("POST"))
.build();
Response invalidate = client.newCall(request).execute();
assertThat(invalidate.body().string()).isEqualTo("B");
assertThat(get(url).body().string()).isEqualTo("C");
}
@Test public void putInvalidatesWithNoContentResponse() throws Exception {
// 1. Seed the cache.
// 2. Invalidate it.
// 3. Expect a cache miss.
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.clearHeaders()
.setResponseCode(HttpURLConnection.HTTP_NO_CONTENT));
server.enqueue(new MockResponse()
.setBody("C"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.put(RequestBody.create("foo", MediaType.get("text/plain")))
.build();
Response invalidate = client.newCall(request).execute();
assertThat(invalidate.body().string()).isEqualTo("");
assertThat(get(url).body().string()).isEqualTo("C");
}
@Test public void etag() throws Exception {
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("ETag: v1"));
assertThat(conditionalRequest.getHeader("If-None-Match")).isEqualTo("v1");
}
/** If both If-Modified-Since and If-None-Match conditions apply, send only If-None-Match. */
@Test public void etagAndExpirationDateInThePast() throws Exception {
String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("ETag: v1")
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
assertThat(conditionalRequest.getHeader("If-None-Match")).isEqualTo("v1");
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isNull();
}
@Test public void etagAndExpirationDateInTheFuture() throws Exception {
assertFullyCached(new MockResponse()
.addHeader("ETag: v1")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
}
@Test public void cacheControlNoCache() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Cache-Control: no-cache"));
}
@Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception {
String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Cache-Control: no-cache"));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test public void pragmaNoCache() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Pragma: no-cache"));
}
@Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception {
String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Pragma: no-cache"));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test public void cacheControlNoStore() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Cache-Control: no-store"));
}
@Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception {
assertNotCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Cache-Control: no-store"));
}
@Test public void partialRangeResponsesDoNotCorruptCache() throws Exception {
// 1. Request a range.
// 2. Request a full document, expecting a cache miss.
server.enqueue(new MockResponse()
.setBody("AA")
.setResponseCode(HttpURLConnection.HTTP_PARTIAL)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Content-Range: bytes 1000-1001/2000"));
server.enqueue(new MockResponse()
.setBody("BB"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.header("Range", "bytes=1000-1001")
.build();
Response range = client.newCall(request).execute();
assertThat(range.body().string()).isEqualTo("AA");
assertThat(get(url).body().string()).isEqualTo("BB");
}
/**
* When the server returns a full response body we will store it and return it regardless of what
* its Last-Modified date is. This behavior was different prior to OkHttp 3.5 when we would prefer
* the response with the later Last-Modified date.
*
* https://github.com/square/okhttp/issues/2886
*/
@Test public void serverReturnsDocumentOlderThanCache() throws Exception {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setBody("B")
.addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("B");
assertThat(get(url).body().string()).isEqualTo("B");
}
@Test public void clientSideNoStore() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("B"));
Request request1 = new Request.Builder()
.url(server.url("/"))
.cacheControl(new CacheControl.Builder().noStore().build())
.build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request2 = new Request.Builder()
.url(server.url("/"))
.build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("B");
}
@Test public void nonIdentityEncodingAndConditionalCache() throws Exception {
assertNonIdentityEncodingCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
}
@Test public void nonIdentityEncodingAndFullCache() throws Exception {
assertNonIdentityEncodingCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
}
private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
server.enqueue(response
.setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
// At least three request/response pairs are required because after the first request is cached
// a different execution path might be taken. Thus modifications to the cache applied during
// the second request might not be visible until another request is performed.
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
}
@Test public void previouslyNotGzippedContentIsNotModifiedAndSpecifiesGzipEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody("ABCABCABC")
.addHeader("Content-Type: text/plain")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
.addHeader("Content-Type: text/plain")
.addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse()
.setBody("DEFDEFDEF"));
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("DEFDEFDEF");
}
@Test public void changedGzippedContentIsNotModifiedAndSpecifiesNewEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody(gzip("ABCABCABC"))
.addHeader("Content-Type: text/plain")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
.addHeader("Content-Type: text/plain")
.addHeader("Content-Encoding: identity"));
server.enqueue(new MockResponse()
.setBody("DEFDEFDEF"));
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("DEFDEFDEF");
}
@Test public void notModifiedSpecifiesEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
.addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse()
.setBody("DEFDEFDEF"));
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("DEFDEFDEF");
}
/** https://github.com/square/okhttp/issues/947 */
@Test public void gzipAndVaryOnAcceptEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip")
.addHeader("Vary: Accept-Encoding")
.addHeader("Cache-Control: max-age=60"));
server.enqueue(new MockResponse()
.setBody("FAIL"));
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
assertThat(get(server.url("/")).body().string()).isEqualTo("ABCABCABC");
}
@Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
clientTestRule.ensureAllConnectionsReleased();
server.enqueue(new MockResponse()
.addHeader("ETag: v1")
.setBody("A"));
server.enqueue(new MockResponse()
.clearHeaders()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(client.connectionPool().idleConnectionCount()).isEqualTo(1);
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
assertConditionallyCached(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)));
}
@Test public void requestMaxAge() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "max-age=30")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void requestMinFresh() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=60")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "min-fresh=120")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void requestMaxStale() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=120")
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "max-stale=180")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("A");
assertThat(response.header("Warning")).isEqualTo(
"110 HttpURLConnection \"Response is stale\"");
}
@Test public void requestMaxStaleDirectiveWithNoValue() throws IOException {
// Add a stale response to the cache.
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=120")
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
// With max-stale, we'll return that stale response.
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "max-stale")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("A");
assertThat(response.header("Warning")).isEqualTo(
"110 HttpURLConnection \"Response is stale\"");
}
@Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=120, must-revalidate")
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "max-stale=180")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
// (no responses enqueued)
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "only-if-cached")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().source().exhausted()).isTrue();
assertThat(response.code()).isEqualTo(504);
assertThat(cache.requestCount()).isEqualTo(1);
assertThat(cache.networkCount()).isEqualTo(0);
assertThat(cache.hitCount()).isEqualTo(0);
}
@Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "only-if-cached")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(2);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(1);
}
@Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "only-if-cached")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().source().exhausted()).isTrue();
assertThat(response.code()).isEqualTo(504);
assertThat(cache.requestCount()).isEqualTo(2);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(0);
}
@Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
server.enqueue(new MockResponse()
.setBody("A"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/"))
.header("Cache-Control", "only-if-cached")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().source().exhausted()).isTrue();
assertThat(response.code()).isEqualTo(504);
assertThat(cache.requestCount()).isEqualTo(2);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(0);
}
@Test public void requestCacheControlNoCache() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.header("Cache-Control", "no-cache")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void requestPragmaNoCache() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.header("Pragma", "no-cache")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception {
MockResponse response = new MockResponse()
.addHeader("ETag: v3")
.addHeader("Cache-Control: max-age=0");
String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
RecordedRequest request =
assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate);
assertThat(request.getHeader("If-Modified-Since")).isEqualTo(ifModifiedSinceDate);
assertThat(request.getHeader("If-None-Match")).isNull();
}
@Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES);
MockResponse response = new MockResponse()
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
.addHeader("Cache-Control: max-age=0");
RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1");
assertThat(request.getHeader("If-None-Match")).isEqualTo("v1");
assertThat(request.getHeader("If-Modified-Since")).isNull();
}
private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
String conditionValue) throws Exception {
server.enqueue(seed.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(url)
.header(conditionName, conditionValue)
.build();
Response response = client.newCall(request).execute();
assertThat(response.code()).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED);
assertThat(response.body().string()).isEqualTo("");
server.takeRequest(); // seed
return server.takeRequest();
}
/**
* For Last-Modified and Date headers, we should echo the date back in the exact format we were
* served.
*/
@Test public void retainServedDateFormat() throws Exception {
// Serve a response with a non-standard date format that OkHttp supports.
Date lastModifiedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1));
Date servedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2));
DateFormat dateFormat = new SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
String lastModifiedString = dateFormat.format(lastModifiedDate);
String servedString = dateFormat.format(servedDate);
// This response should be conditionally cached.
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + lastModifiedString)
.addHeader("Expires: " + servedString)
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
// The first request has no conditions.
RecordedRequest request1 = server.takeRequest();
assertThat(request1.getHeader("If-Modified-Since")).isNull();
// The 2nd request uses the server's date format.
RecordedRequest request2 = server.takeRequest();
assertThat(request2.getHeader("If-Modified-Since")).isEqualTo(lastModifiedString);
}
@Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Request request = new Request.Builder()
.url(server.url("/"))
.header("If-Modified-Since", formatDate(-24, TimeUnit.HOURS))
.build();
Response response = client.newCall(request).execute();
assertThat(response.code()).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED);
assertThat(response.body().string()).isEqualTo("");
}
@Test public void authorizationRequestFullyCached() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.header("Authorization", "password")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("A");
}
@Test public void contentLocationDoesNotPopulateCache() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Content-Location: /bar")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/foo")).body().string()).isEqualTo("A");
assertThat(get(server.url("/bar")).body().string()).isEqualTo("B");
}
@Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/a")).body().string()).isEqualTo("A");
assertThat(get(server.url("/a")).body().string()).isEqualTo("A");
assertThat(get(server.url("/b")).body().string()).isEqualTo("B");
assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0);
assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(1);
assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(2);
}
@Test public void statisticsConditionalCacheMiss() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
server.enqueue(new MockResponse()
.setBody("C"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(1);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(0);
assertThat(get(server.url("/")).body().string()).isEqualTo("B");
assertThat(get(server.url("/")).body().string()).isEqualTo("C");
assertThat(cache.requestCount()).isEqualTo(3);
assertThat(cache.networkCount()).isEqualTo(3);
assertThat(cache.hitCount()).isEqualTo(0);
}
@Test public void statisticsConditionalCacheHit() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(1);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(0);
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(3);
assertThat(cache.networkCount()).isEqualTo(3);
assertThat(cache.hitCount()).isEqualTo(2);
}
@Test public void statisticsFullCacheHit() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(1);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(0);
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(cache.requestCount()).isEqualTo(3);
assertThat(cache.networkCount()).isEqualTo(1);
assertThat(cache.hitCount()).isEqualTo(2);
}
@Test public void varyMatchesChangedRequestHeaderField() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request frRequest = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.build();
Response frResponse = client.newCall(frRequest).execute();
assertThat(frResponse.body().string()).isEqualTo("A");
Request enRequest = new Request.Builder()
.url(url)
.header("Accept-Language", "en-US")
.build();
Response enResponse = client.newCall(enRequest).execute();
assertThat(enResponse.body().string()).isEqualTo("B");
}
@Test public void varyMatchesUnchangedRequestHeaderField() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.build();
Response response1 = client.newCall(request).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request1 = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.build();
Response response2 = client.newCall(request1).execute();
assertThat(response2.body().string()).isEqualTo("A");
}
@Test public void varyMatchesAbsentRequestHeaderField() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
}
@Test public void varyMatchesAddedRequestHeaderField() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/")).header("Foo", "bar")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void varyMatchesRemovedRequestHeaderField() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
Request request = new Request.Builder()
.url(server.url("/")).header("Foo", "bar")
.build();
Response fooresponse = client.newCall(request).execute();
assertThat(fooresponse.body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("B");
}
@Test public void varyFieldsAreCaseInsensitive() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: ACCEPT-LANGUAGE")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.build();
Response response1 = client.newCall(request).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request1 = new Request.Builder()
.url(url)
.header("accept-language", "fr-CA")
.build();
Response response2 = client.newCall(request1).execute();
assertThat(response2.body().string()).isEqualTo("A");
}
@Test public void varyMultipleFieldsWithMatch() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language, Accept-Charset")
.addHeader("Vary: Accept-Encoding")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.header("Accept-Charset", "UTF-8")
.header("Accept-Encoding", "identity")
.build();
Response response1 = client.newCall(request).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request1 = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.header("Accept-Charset", "UTF-8")
.header("Accept-Encoding", "identity")
.build();
Response response2 = client.newCall(request1).execute();
assertThat(response2.body().string()).isEqualTo("A");
}
@Test public void varyMultipleFieldsWithNoMatch() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language, Accept-Charset")
.addHeader("Vary: Accept-Encoding")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request frRequest = new Request.Builder()
.url(url)
.header("Accept-Language", "fr-CA")
.header("Accept-Charset", "UTF-8")
.header("Accept-Encoding", "identity")
.build();
Response frResponse = client.newCall(frRequest).execute();
assertThat(frResponse.body().string()).isEqualTo("A");
Request enRequest = new Request.Builder()
.url(url)
.header("Accept-Language", "en-CA")
.header("Accept-Charset", "UTF-8")
.header("Accept-Encoding", "identity")
.build();
Response enResponse = client.newCall(enRequest).execute();
assertThat(enResponse.body().string()).isEqualTo("B");
}
@Test public void varyMultipleFieldValuesWithMatch() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request1 = new Request.Builder()
.url(url)
.addHeader("Accept-Language", "fr-CA, fr-FR")
.addHeader("Accept-Language", "en-US")
.build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request2 = new Request.Builder()
.url(url)
.addHeader("Accept-Language", "fr-CA, fr-FR")
.addHeader("Accept-Language", "en-US")
.build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("A");
}
@Test public void varyMultipleFieldValuesWithNoMatch() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
Request request1 = new Request.Builder()
.url(url)
.addHeader("Accept-Language", "fr-CA, fr-FR")
.addHeader("Accept-Language", "en-US")
.build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request2 = new Request.Builder()
.url(url)
.addHeader("Accept-Language", "fr-CA")
.addHeader("Accept-Language", "en-US")
.build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("B");
}
@Test public void varyAsterisk() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: *")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
assertThat(get(server.url("/")).body().string()).isEqualTo("B");
}
@Test public void varyAndHttps() throws Exception {
server.useHttps(handshakeCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
client = client.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build();
HttpUrl url = server.url("/");
Request request1 = new Request.Builder()
.url(url)
.header("Accept-Language", "en-US")
.build();
Response response1 = client.newCall(request1).execute();
assertThat(response1.body().string()).isEqualTo("A");
Request request2 = new Request.Builder()
.url(url)
.header("Accept-Language", "en-US")
.build();
Response response2 = client.newCall(request2).execute();
assertThat(response2.body().string()).isEqualTo("A");
}
@Test public void cachePlusCookies() throws Exception {
RecordingCookieJar cookieJar = new RecordingCookieJar();
client = client.newBuilder()
.cookieJar(cookieJar)
.build();
server.enqueue(new MockResponse()
.addHeader("Set-Cookie: a=FIRST")
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Set-Cookie: a=SECOND")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
cookieJar.assertResponseCookies("a=FIRST; path=/");
assertThat(get(url).body().string()).isEqualTo("A");
cookieJar.assertResponseCookies("a=SECOND; path=/");
}
@Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Allow: GET, HEAD")
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Allow: GET, HEAD, PUT")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.header("Allow")).isEqualTo("GET, HEAD");
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Allow")).isEqualTo("GET, HEAD, PUT");
}
@Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Transfer-Encoding: identity")
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Transfer-Encoding: none")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.header("Transfer-Encoding")).isEqualTo("identity");
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Transfer-Encoding")).isEqualTo("identity");
}
@Test public void getHeadersDeletesCached100LevelWarnings() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Warning: 199 test danger")
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.header("Warning")).isEqualTo("199 test danger");
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Warning")).isNull();
}
@Test public void getHeadersRetainsCached200LevelWarnings() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Warning: 299 test danger")
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.header("Warning")).isEqualTo("299 test danger");
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Warning")).isEqualTo("299 test danger");
}
@Test public void doNotCachePartialResponse() throws Exception {
assertNotCached(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_PARTIAL)
.addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
.addHeader("Content-Range: bytes 100-100/200")
.addHeader("Cache-Control: max-age=60"));
}
@Test public void conditionalHitUpdatesCache() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=30")
.addHeader("Allow: GET, HEAD")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse()
.setBody("B"));
// A cache miss writes the cache.
long t0 = System.currentTimeMillis();
Response response1 = get(server.url("/a"));
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.header("Allow")).isNull();
assertThat((double) (response1.receivedResponseAtMillis() - t0)).isCloseTo(0, offset(250.0));
// A conditional cache hit updates the cache.
Thread.sleep(500); // Make sure t0 and t1 are distinct.
long t1 = System.currentTimeMillis();
Response response2 = get(server.url("/a"));
assertThat(response2.code()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.header("Allow")).isEqualTo("GET, HEAD");
Long updatedTimestamp = response2.receivedResponseAtMillis();
assertThat((double) (updatedTimestamp - t1)).isCloseTo(0, offset(250.0));
// A full cache hit reads the cache.
Thread.sleep(10);
Response response3 = get(server.url("/a"));
assertThat(response3.body().string()).isEqualTo("A");
assertThat(response3.header("Allow")).isEqualTo("GET, HEAD");
assertThat(response3.receivedResponseAtMillis()).isEqualTo(updatedTimestamp);
assertThat(server.getRequestCount()).isEqualTo(2);
}
@Test public void responseSourceHeaderCached() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Request request = new Request.Builder()
.url(server.url("/")).header("Cache-Control", "only-if-cached")
.build();
Response response = client.newCall(request).execute();
assertThat(response.body().string()).isEqualTo("A");
}
@Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setBody("B")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Response response = get(server.url("/"));
assertThat(response.body().string()).isEqualTo("B");
}
@Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
server.enqueue(new MockResponse()
.setBody("A")
.addHeader("Cache-Control: max-age=0")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse()
.setResponseCode(304));
assertThat(get(server.url("/")).body().string()).isEqualTo("A");
Response response = get(server.url("/"));
assertThat(response.body().string()).isEqualTo("A");
}
@Test public void responseSourceHeaderFetched() throws IOException {
server.enqueue(new MockResponse()
.setBody("A"));
Response response = get(server.url("/"));
assertThat(response.body().string()).isEqualTo("A");
}
@Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
Headers.Builder headers = new Headers.Builder()
.add("Cache-Control: max-age=120");
Internal.addHeaderLenient(headers, ": A");
server.enqueue(new MockResponse()
.setHeaders(headers.build())
.setBody("body"));
Response response = get(server.url("/"));
assertThat(response.header("")).isEqualTo("A");
assertThat(response.body().string()).isEqualTo("body");
}
/**
* Old implementations of OkHttp's response cache wrote header fields like ":status: 200 OK". This
* broke our cached response parser because it split on the first colon. This regression test
* exists to help us read these old bad cache entries.
*
* https://github.com/square/okhttp/issues/227
*/
@Test public void testGoldenCacheResponse() throws Exception {
cache.close();
server.enqueue(new MockResponse()
.clearHeaders()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
HttpUrl url = server.url("/");
String urlKey = Cache.key(url);
String entryMetadata = ""
+ "" + url + "\n"
+ "GET\n"
+ "0\n"
+ "HTTP/1.1 200 OK\n"
+ "7\n"
+ ":status: 200 OK\n"
+ ":version: HTTP/1.1\n"
+ "etag: foo\n"
+ "content-length: 3\n"
+ "OkHttp-Received-Millis: " + System.currentTimeMillis() + "\n"
+ "X-Android-Response-Source: NETWORK 200\n"
+ "OkHttp-Sent-Millis: " + System.currentTimeMillis() + "\n"
+ "\n"
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n"
+ "1\n"
+ "MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgy"
+ "OTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEF"
+ "AAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUc"
+ "ltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFK"
+ "v6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP"
+ "7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dm"
+ "yFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg==\n"
+ "-1\n";
String entryBody = "abc";
String journalBody = ""
+ "libcore.io.DiskLruCache\n"
+ "1\n"
+ "201105\n"
+ "2\n"
+ "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
Response response = get(url);
assertThat(response.body().string()).isEqualTo(entryBody);
assertThat(response.header("Content-Length")).isEqualTo("3");
assertThat(response.header("etag")).isEqualTo("foo");
}
/** Exercise the cache format in OkHttp 2.7 and all earlier releases. */
@Test public void testGoldenCacheHttpsResponseOkHttp27() throws Exception {
HttpUrl url = server.url("/");
String urlKey = Cache.key(url);
String prefix = Platform.get().getPrefix();
String entryMetadata = ""
+ "" + url + "\n"
+ "GET\n"
+ "0\n"
+ "HTTP/1.1 200 OK\n"
+ "4\n"
+ "Content-Length: 3\n"
+ prefix + "-Received-Millis: " + System.currentTimeMillis() + "\n"
+ prefix + "-Sent-Millis: " + System.currentTimeMillis() + "\n"
+ "Cache-Control: max-age=60\n"
+ "\n"
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n"
+ "1\n"
+ "MIIBnDCCAQWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUxMjIyMDEx"
+ "MTQwWhcNMTUxMjIzMDExMTQwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ"
+ "AoGBAJTn2Dh8xYmegvpOSmsKb2Os6Cxf1L4fYbnHr/turInUD5r1P7ZAuxurY880q3GT5bUDoirS3IfucddrT1Ac"
+ "AmUzEmk/FDjggiP8DlxFkY/XwXBlhRDVIp/mRuASPMGInckc0ZaixOkRFyrxADj+r1eaSmXCIvV5yTY6IaIokLj1"
+ "AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAFblnedqtfRqI9j2WDyPPoG0NTZf9xwjeUu+ju+Ktty8u9k7Lgrrd/DH2"
+ "mQEtBD1Ctvp91MJfAClNg3faZzwClUyu5pd0QXRZEUwSwZQNen2QWDHRlVsItclBJ4t+AJLqTbwofWi4m4K8REOl"
+ "593hD55E4+lY22JZiVQyjsQhe6I=\n"
+ "0\n";
String entryBody = "abc";
String journalBody = ""
+ "libcore.io.DiskLruCache\n"
+ "1\n"
+ "201105\n"
+ "2\n"
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
Response response = get(url);
assertThat(response.body().string()).isEqualTo(entryBody);
assertThat(response.header("Content-Length")).isEqualTo("3");
}
/** The TLS version is present in OkHttp 3.0 and beyond. */
@Test public void testGoldenCacheHttpsResponseOkHttp30() throws Exception {
HttpUrl url = server.url("/");
String urlKey = Cache.key(url);
String prefix = Platform.get().getPrefix();
String entryMetadata = ""
+ "" + url + "\n"
+ "GET\n"
+ "0\n"
+ "HTTP/1.1 200 OK\n"
+ "4\n"
+ "Content-Length: 3\n"
+ prefix + "-Received-Millis: " + System.currentTimeMillis() + "\n"
+ prefix + "-Sent-Millis: " + System.currentTimeMillis() + "\n"
+ "Cache-Control: max-age=60\n"
+ "\n"
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n"
+ "1\n"
+ "MIIBnDCCAQWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUxMjIyMDEx"
+ "MTQwWhcNMTUxMjIzMDExMTQwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ"
+ "AoGBAJTn2Dh8xYmegvpOSmsKb2Os6Cxf1L4fYbnHr/turInUD5r1P7ZAuxurY880q3GT5bUDoirS3IfucddrT1Ac"
+ "AmUzEmk/FDjggiP8DlxFkY/XwXBlhRDVIp/mRuASPMGInckc0ZaixOkRFyrxADj+r1eaSmXCIvV5yTY6IaIokLj1"
+ "AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAFblnedqtfRqI9j2WDyPPoG0NTZf9xwjeUu+ju+Ktty8u9k7Lgrrd/DH2"
+ "mQEtBD1Ctvp91MJfAClNg3faZzwClUyu5pd0QXRZEUwSwZQNen2QWDHRlVsItclBJ4t+AJLqTbwofWi4m4K8REOl"
+ "593hD55E4+lY22JZiVQyjsQhe6I=\n"
+ "0\n"
+ "TLSv1.2\n";
String entryBody = "abc";
String journalBody = ""
+ "libcore.io.DiskLruCache\n"
+ "1\n"
+ "201105\n"
+ "2\n"
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
Response response = get(url);
assertThat(response.body().string()).isEqualTo(entryBody);
assertThat(response.header("Content-Length")).isEqualTo("3");
}
@Test public void testGoldenCacheHttpResponseOkHttp30() throws Exception {
HttpUrl url = server.url("/");
String urlKey = Cache.key(url);
String prefix = Platform.get().getPrefix();
String entryMetadata = ""
+ "" + url + "\n"
+ "GET\n"
+ "0\n"
+ "HTTP/1.1 200 OK\n"
+ "4\n"
+ "Cache-Control: max-age=60\n"
+ "Content-Length: 3\n"
+ prefix + "-Received-Millis: " + System.currentTimeMillis() + "\n"
+ prefix + "-Sent-Millis: " + System.currentTimeMillis() + "\n";
String entryBody = "abc";
String journalBody = ""
+ "libcore.io.DiskLruCache\n"
+ "1\n"
+ "201105\n"
+ "2\n"
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
Response response = get(url);
assertThat(response.body().string()).isEqualTo(entryBody);
assertThat(response.header("Content-Length")).isEqualTo("3");
}
@Test public void evictAll() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
client.cache().evictAll();
assertThat(client.cache().size()).isEqualTo(0);
assertThat(get(url).body().string()).isEqualTo("B");
}
@Test public void networkInterceptorInvokedForConditionalGet() throws Exception {
server.enqueue(new MockResponse()
.addHeader("ETag: v1")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
// Seed the cache.
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
final AtomicReference ifNoneMatch = new AtomicReference<>();
client = client.newBuilder()
.addNetworkInterceptor(chain -> {
ifNoneMatch.compareAndSet(null, chain.request().header("If-None-Match"));
return chain.proceed(chain.request());
})
.build();
// Confirm the value is cached and intercepted.
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(ifNoneMatch.get()).isEqualTo("v1");
}
@Test public void networkInterceptorNotInvokedForFullyCached() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
// Seed the cache.
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
// Confirm the interceptor isn't exercised.
client = client.newBuilder()
.addNetworkInterceptor(chain -> { throw new AssertionError(); })
.build();
assertThat(get(url).body().string()).isEqualTo("A");
}
@Test public void iterateCache() throws Exception {
// Put some responses in the cache.
server.enqueue(new MockResponse()
.setBody("a"));
HttpUrl urlA = server.url("/a");
assertThat(get(urlA).body().string()).isEqualTo("a");
server.enqueue(new MockResponse()
.setBody("b"));
HttpUrl urlB = server.url("/b");
assertThat(get(urlB).body().string()).isEqualTo("b");
server.enqueue(new MockResponse()
.setBody("c"));
HttpUrl urlC = server.url("/c");
assertThat(get(urlC).body().string()).isEqualTo("c");
// Confirm the iterator returns those responses...
Iterator i = cache.urls();
assertThat(i.hasNext()).isTrue();
assertThat(i.next()).isEqualTo(urlA.toString());
assertThat(i.hasNext()).isTrue();
assertThat(i.next()).isEqualTo(urlB.toString());
assertThat(i.hasNext()).isTrue();
assertThat(i.next()).isEqualTo(urlC.toString());
// ... and nothing else.
assertThat(i.hasNext()).isFalse();
try {
i.next();
fail();
} catch (NoSuchElementException expected) {
}
}
@Test public void iteratorRemoveFromCache() throws Exception {
// Put a response in the cache.
server.enqueue(new MockResponse()
.addHeader("Cache-Control: max-age=60")
.setBody("a"));
HttpUrl url = server.url("/a");
assertThat(get(url).body().string()).isEqualTo("a");
// Remove it with iteration.
Iterator i = cache.urls();
assertThat(i.next()).isEqualTo(url.toString());
i.remove();
// Confirm that subsequent requests suffer a cache miss.
server.enqueue(new MockResponse()
.setBody("b"));
assertThat(get(url).body().string()).isEqualTo("b");
}
@Test public void iteratorRemoveWithoutNextThrows() throws Exception {
// Put a response in the cache.
server.enqueue(new MockResponse()
.setBody("a"));
HttpUrl url = server.url("/a");
assertThat(get(url).body().string()).isEqualTo("a");
Iterator i = cache.urls();
assertThat(i.hasNext()).isTrue();
try {
i.remove();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void iteratorRemoveOncePerCallToNext() throws Exception {
// Put a response in the cache.
server.enqueue(new MockResponse()
.setBody("a"));
HttpUrl url = server.url("/a");
assertThat(get(url).body().string()).isEqualTo("a");
Iterator i = cache.urls();
assertThat(i.next()).isEqualTo(url.toString());
i.remove();
// Too many calls to remove().
try {
i.remove();
fail();
} catch (IllegalStateException expected) {
}
}
@Test public void elementEvictedBetweenHasNextAndNext() throws Exception {
// Put a response in the cache.
server.enqueue(new MockResponse()
.setBody("a"));
HttpUrl url = server.url("/a");
assertThat(get(url).body().string()).isEqualTo("a");
// The URL will remain available if hasNext() returned true...
Iterator i = cache.urls();
assertThat(i.hasNext()).isTrue();
// ...so even when we evict the element, we still get something back.
cache.evictAll();
assertThat(i.next()).isEqualTo(url.toString());
// Remove does nothing. But most importantly, it doesn't throw!
i.remove();
}
@Test public void elementEvictedBeforeHasNextIsOmitted() throws Exception {
// Put a response in the cache.
server.enqueue(new MockResponse()
.setBody("a"));
HttpUrl url = server.url("/a");
assertThat(get(url).body().string()).isEqualTo("a");
Iterator i = cache.urls();
cache.evictAll();
// The URL was evicted before hasNext() made any promises.
assertThat(i.hasNext()).isFalse();
try {
i.next();
fail();
} catch (NoSuchElementException expected) {
}
}
/** Test https://github.com/square/okhttp/issues/1712. */
@Test public void conditionalMissUpdatesCache() throws Exception {
server.enqueue(new MockResponse()
.addHeader("ETag: v1")
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse()
.addHeader("ETag: v2")
.setBody("B"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("B");
assertThat(get(url).body().string()).isEqualTo("B");
assertThat(server.takeRequest().getHeader("If-None-Match")).isNull();
assertThat(server.takeRequest().getHeader("If-None-Match")).isEqualTo("v1");
assertThat(server.takeRequest().getHeader("If-None-Match")).isEqualTo("v1");
assertThat(server.takeRequest().getHeader("If-None-Match")).isEqualTo("v2");
}
@Test public void combinedCacheHeadersCanBeNonAscii() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Cache-Control: max-age=0")
.addHeaderLenient("Alpha", "α")
.addHeaderLenient("β", "Beta")
.setBody("abcd"));
server.enqueue(new MockResponse()
.addHeader("Transfer-Encoding: none")
.addHeaderLenient("Gamma", "Γ")
.addHeaderLenient("Δ", "Delta")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.header("Alpha")).isEqualTo("α");
assertThat(response1.header("β")).isEqualTo("Beta");
assertThat(response1.body().string()).isEqualTo("abcd");
Response response2 = get(server.url("/"));
assertThat(response2.header("Alpha")).isEqualTo("α");
assertThat(response2.header("β")).isEqualTo("Beta");
assertThat(response2.header("Gamma")).isEqualTo("Γ");
assertThat(response2.header("Δ")).isEqualTo("Delta");
assertThat(response2.body().string()).isEqualTo("abcd");
}
@Test public void etagConditionCanBeNonAscii() throws Exception {
server.enqueue(new MockResponse()
.addHeaderLenient("Etag", "α")
.addHeader("Cache-Control: max-age=0")
.setBody("abcd"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("abcd");
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("abcd");
assertThat(server.takeRequest().getHeader("If-None-Match")).isNull();
assertThat(server.takeRequest().getHeader("If-None-Match")).isEqualTo("α");
}
@Test public void conditionalHitHeadersCombined() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Etag", "a")
.addHeader("Cache-Control: max-age=0")
.addHeader("A: a1")
.addHeader("B: b2")
.addHeader("B: b3")
.setBody("abcd"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
.addHeader("B: b4")
.addHeader("B: b5")
.addHeader("C: c6"));
Response response1 = get(server.url("/"));
assertThat(response1.body().string()).isEqualTo("abcd");
assertThat(response1.headers()).isEqualTo(Headers.of("Etag", "a", "Cache-Control", "max-age=0",
"A", "a1", "B", "b2", "B", "b3", "Content-Length", "4"));
// The original 'A' header is retained because the network response doesn't have one.
// The original 'B' headers are replaced by the network response.
// The network's 'C' header is added.
Response response2 = get(server.url("/"));
assertThat(response2.body().string()).isEqualTo("abcd");
assertThat(response2.headers()).isEqualTo(Headers.of("Etag", "a", "Cache-Control", "max-age=0",
"A", "a1", "Content-Length", "4", "B", "b4", "B", "b5", "C", "c6"));
}
private Response get(HttpUrl url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
return client.newCall(request).execute();
}
private void writeFile(Path directory, String file, String content) throws IOException {
BufferedSink sink = Okio.buffer(fileSystem.sink(directory.resolve(file)));
sink.writeUtf8(content);
sink.close();
}
/**
* @param delta the offset from the current date to use. Negative values yield dates in the past;
* positive values yield dates in the future.
*/
private String formatDate(long delta, TimeUnit timeUnit) {
return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
}
private String formatDate(Date date) {
DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
rfc1123.setTimeZone(TimeZone.getTimeZone("GMT"));
return rfc1123.format(date);
}
private void assertNotCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("B");
}
/** @return the request with the conditional get headers. */
private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception {
// scenario 1: condition succeeds
server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
// scenario 2: condition fails
server.enqueue(response.setBody("B")
.setStatus("HTTP/1.1 200 B-OK"));
server.enqueue(new MockResponse()
.setStatus("HTTP/1.1 200 C-OK")
.setBody("C"));
HttpUrl valid = server.url("/valid");
Response response1 = get(valid);
assertThat(response1.body().string()).isEqualTo("A");
assertThat(response1.code()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(response1.message()).isEqualTo("A-OK");
Response response2 = get(valid);
assertThat(response2.body().string()).isEqualTo("A");
assertThat(response2.code()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(response2.message()).isEqualTo("A-OK");
HttpUrl invalid = server.url("/invalid");
Response response3 = get(invalid);
assertThat(response3.body().string()).isEqualTo("B");
assertThat(response3.code()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(response3.message()).isEqualTo("B-OK");
Response response4 = get(invalid);
assertThat(response4.body().string()).isEqualTo("C");
assertThat(response4.code()).isEqualTo(HttpURLConnection.HTTP_OK);
assertThat(response4.message()).isEqualTo("C-OK");
server.takeRequest(); // regular get
return server.takeRequest(); // conditional get
}
@Test public void immutableIsCached() throws Exception {
server.enqueue(new MockResponse()
.addHeader("Cache-Control", "immutable, max-age=10")
.setBody("A"));
server.enqueue(new MockResponse()
.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("A");
}
@Test public void immutableIsCachedAfterMultipleCalls() throws Exception {
server.enqueue(new MockResponse()
.setBody("A"));
server.enqueue(new MockResponse()
.addHeader("Cache-Control", "immutable, max-age=10")
.setBody("B"));
server.enqueue(new MockResponse()
.setBody("C"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("B");
assertThat(get(url).body().string()).isEqualTo("B");
}
@Test public void immutableIsNotCachedBeyondFreshnessLifetime() throws Exception {
// last modified: 115 seconds ago
// served: 15 seconds ago
// default lifetime: (115 - 15) / 10 = 10 seconds
// expires: 10 seconds from served date = 5 seconds ago
String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
.addHeader("Cache-Control: immutable")
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
assertThat(conditionalRequest.getHeader("If-Modified-Since")).isEqualTo(
lastModifiedDate);
}
@Test
public void testPublicPathConstructor() throws IOException {
List events = new ArrayList<>();
fileSystem.createDirectories(cache.directoryPath());
fileSystem.createDirectories(cache.directoryPath());
FileSystem loggingFileSystem = new ForwardingFileSystem(fileSystem) {
@Override
public Path onPathParameter(Path path, java.lang.String functionName, java.lang.String parameterName) {
events.add(functionName + ":" + path);
return path;
}
@Override
public Path onPathResult(Path path, java.lang.String functionName) {
events.add(functionName + ":" + path);
return path;
}
};
Path path = Path.get("/cache");
Cache c = new Cache(path, 100000L, loggingFileSystem);
assertThat(c.directoryPath()).isEqualTo(path);
c.size();
assertThat(events).containsExactly("metadataOrNull:/cache/journal.bkp",
"metadataOrNull:/cache",
"sink:/cache/journal.bkp",
"delete:/cache/journal.bkp",
"metadataOrNull:/cache/journal",
"metadataOrNull:/cache",
"sink:/cache/journal.tmp",
"metadataOrNull:/cache/journal",
"atomicMove:/cache/journal.tmp",
"atomicMove:/cache/journal",
"appendingSink:/cache/journal");
events.clear();
c.size();
assertThat(events).isEmpty();
}
private void assertFullyCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(response.setBody("B"));
HttpUrl url = server.url("/");
assertThat(get(url).body().string()).isEqualTo("A");
assertThat(get(url).body().string()).isEqualTo("A");
}
/**
* Shortens the body of {@code response} but not the corresponding headers. Only useful to test
* how clients respond to the premature conclusion of the HTTP body.
*/
private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
response.setSocketPolicy(DISCONNECT_AT_END);
Headers headers = response.getHeaders();
Buffer truncatedBody = new Buffer();
truncatedBody.write(response.getBody(), numBytesToKeep);
response.setBody(truncatedBody);
response.setHeaders(headers);
return response;
}
enum TransferKind {
CHUNKED {
@Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setChunkedBody(content, chunkSize);
}
},
FIXED_LENGTH {
@Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
}
},
END_OF_STREAM {
@Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
response.setSocketPolicy(DISCONNECT_AT_END);
response.removeHeader("Content-Length");
}
};
abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException;
void setBody(MockResponse response, String content, int chunkSize) throws IOException {
setBody(response, new Buffer().writeUtf8(content), chunkSize);
}
}
/** Returns a gzipped copy of {@code bytes}. */
public Buffer gzip(String data) throws IOException {
Buffer result = new Buffer();
BufferedSink sink = Okio.buffer(new GzipSink(result));
sink.writeUtf8(data);
sink.close();
return result;
}
}