1. Introduction
This tutorial is going to cover about how to cache response with OkHttp, an HTTP & HTTP/2 client for Android and Java applications.
Fetching resource over the network is both slow and expensive. Therefore, OkHttp provides us ability to cache and reuse previously fetched resources similarly to many web browsers. Note that although OkHttpClient honors all HTTP/1.1 (RFC 7234) cache headers, it doesn’t cache partial responses.
2. How to Cache Response with OkHttp
To cache response with OkHttp, we’ll need:
- A directory which can be read and write to, is used for cache storing.
- The cache directory should be private, and untrusted applications should not be able to read its contents.
- The same cache directory should not be used for multiple caches because accessing the same cache directory simultaneously causes error.
- Most applications should call new OkHttpClient() exactly once, configure it with their cache, and use that same instance everywhere.
3. Cache Response with OkHttp Example
This section illustrates how we create an OkHttpClient and fetch resources from a remote HTTP request and response service which its responses will be cached in 20 seconds: http://httpbin.org/cache/20.
3.1. Preparation
We’re going to use OkHttp, version 3.5.0 for the example, and the only Maven dependency we will need is:
1 2 3 4 5 |
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.5.0</version> </dependency> |
3.2. Example
Let’s see a part of a JUnit test class called CacheOkHttpExampleTest which will be used to illustrate some cache feature of OkHttp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class CacheOkHttpExampleTest { private OkHttpClient client; @Before public void initCache() throws IOException { int cacheSize = 10 * 1024 * 1024; // 10 MiB File cacheLoc = new File("/var/tmp/okhttp"); Cache cache = new Cache(cacheLoc, cacheSize); // Clear all previous caches on the given directory to make sure // all our tests run correctly cache.evictAll(); client = new OkHttpClient.Builder().cache(cache).build(); } // more test methods } |
The initCache() method which is annotated with the @Before annotation, is used to initialize the OkHttpClient field and provide it a cache directory with 10MB size limit before any test method. The cache.evictAll() method is used to clear all the previous caches stored on the given directory to make sure all the tests will run correctly.
Here is an example of caching response with OkHttp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Test public void cacheWithOkHttpTest() throws IOException { // The below URL will produce the response with cache in 20 seconds Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response1 = client.newCall(request).execute(); if (!response1.isSuccessful()) { throw new IOException("Unexpected code " + response1); } response1.close(); assertNull(response1.cacheResponse()); assertNotNull(response1.networkResponse()); Response response2 = client.newCall(request).execute(); if (!response2.isSuccessful()) { throw new IOException("Unexpected code " + response2); } response2.close(); assertNotNull(response2.cacheResponse()); assertNull(response2.networkResponse()); } |
First, we create a request to a remote HTTP service which will allow the response to be cached in 20 seconds, and then we execute the request:
1 2 |
Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response1 = client.newCall(request).execute(); |
Because this is the first request to the remote HTTP service, there was no response in cache. The client has to fetch resource directly from the server and then cache the response. As a result, the raw response which received from the network will be used and the response in cache is NULL.
1 2 |
assertNull(response1.cacheResponse()); assertNotNull(response1.networkResponse()); |
Next, we use the same client to execute another request to the remote HTTP service:
1 2 3 4 |
Response response2 = client.newCall(request).execute(); if (!response2.isSuccessful()) { throw new IOException("Unexpected code " + response2); } |
Because the response of the previous is in cache and is still fresh, the OkHttClient will use it instead of fetching from the remote server (The network response is NULL).
1 2 |
assertNotNull(response2.cacheResponse()); assertNull(response2.networkResponse()); |
4. Cache Optimization
This section is going to cover some situations that we can optimize cache from the OkHttp client side.
4.1. Force a Network Response
In some situations, such as after a user clicks a ‘refresh’ button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh or force a network response, add the no-cache directive. Let’s see an example below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Test public void forceNetworkResponseTest() throws IOException { // The below URL will produce the response with cache in 20 seconds Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response4 = client.newCall(request).execute(); if (!response4.isSuccessful()) { throw new IOException("Unexpected code " + response4); } response4.close(); assertNull(response4.cacheResponse()); assertNotNull(response4.networkResponse()); request = new Request.Builder().cacheControl(new CacheControl.Builder().noCache().build()) .url("http://httpbin.org/cache/20").build(); Response response5 = client.newCall(request).execute(); if (!response5.isSuccessful()) { throw new IOException("Unexpected code " + response5); } response5.close(); assertNull(response5.cacheResponse()); assertNotNull(response5.networkResponse()); } |
At first, we create a request to the remote HTTP service and execute the request.
1 2 |
Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response4 = client.newCall(request).execute(); |
In similar to the first example, because the above request is the first request to server, there was no response in cache and the client will fetch resource directly from server and use it.
1 2 |
assertNull(response4.cacheResponse()); assertNotNull(response4.networkResponse()); |
Note that response4, the response of this request, will be cached.
The next , we create a new request which doesn’t accept an unvalidated cached response, to the same remote HTTP service.
1 2 |
request = new Request.Builder().cacheControl(new CacheControl.Builder().noCache().build()) .url("http://httpbin.org/cache/20").build(); |
Then we use the same client created above to execute the request:
1 |
Response response5 = client.newCall(request).execute(); |
Even the previous response was in cache and is still fresh (reponse4), the client ignored it and did fetch the resource directly from the remote HTTP service.
1 2 |
assertNull(response5.cacheResponse()); assertNotNull(response5.networkResponse()); |
4.2. Force a Cache Response
Sometimes, we’ll want to show resources if they are available immediately, but not otherwise. This can be used so our application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources or force a cache response, add the only-if-cached directive. Let’s see an example below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@Test public void forceCacheResponseTest() throws Exception { // The below URL will produce the response with cache in 20 seconds Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response6 = client.newCall(request).execute(); if (!response6.isSuccessful()) { throw new IOException("Unexpected code " + response6); } response6.close(); assertNull(response6.cacheResponse()); assertNotNull(response6.networkResponse()); // Only accept the response if it is in the cache. If the response isn't // cached, a 504 Unsatisfiable Request response will be returned. request = new Request.Builder() .cacheControl(new CacheControl.Builder().onlyIfCached().build()) .url("http://httpbin.org/cache/20").build(); Response response7 = client.newCall(request).execute(); response7.close(); assertNotNull(response7.cacheResponse()); assertNull(response7.networkResponse()); // Sleep 20 seconds to make sure the cache will be stale. Then the 504 // response error will be return. Thread.sleep(20000); Response response8 = client.newCall(request).execute(); response8.close(); assertEquals(504, response8.code()); assertEquals("Unsatisfiable Request (only-if-cached)", response8.message()); } |
Firstly, we create a normal request to the remote HTTP service to fetch the resource and cache the response (response6)
1 2 |
Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response6 = client.newCall(request).execute(); |
Secondly, we create a new request which accepts only the response if it is in the cache, to the same remote HTTP service. We do this by adding the only-if-cached directive:
1 2 3 |
request = new Request.Builder() .cacheControl(new CacheControl.Builder().onlyIfCached().build()) .url("http://httpbin.org/cache/20").build(); |
Then we execute the request:
1 |
Response response7 = client.newCall(request).execute(); |
Let’s verify the response:
1 2 |
assertNotNull(response7.cacheResponse()); assertNull(response7.networkResponse()); |
This request will be execute successfully because the response in cache(reponse6) is still refresh and returned . Note that if the response isn’t available in the cache or requires server validation, the call will fail with a 504 Unsatisfiable Request.
Thirdly, we make the current thread sleep in 20 seconds to make sure the response in cache is stale. Then we execute the request again:
1 2 |
Thread.sleep(20000); Response response8 = client.newCall(request).execute(); |
Because the response in cache was stale, a 504 Unsatisfiable Request response was returned.
1 2 |
assertEquals(504, response8.code()); assertEquals("Unsatisfiable Request (only-if-cached)", response8.message()); |
4.2.1. Permit Stale Cached Responses
In some situations, a stale response is better than no response. To permit stale cached responses, we can use the max-stale directive with the maximum staleness in seconds:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@Test public void forceCacheResponseAllowStaleTest() throws Exception { // The below URL will produce the response with cache in 20 seconds Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response9 = client.newCall(request).execute(); if (!response9.isSuccessful()) { throw new IOException("Unexpected code " + response9); } response9.close(); assertNull(response9.cacheResponse()); assertNotNull(response9.networkResponse()); // Sleep 20 seconds to make sure the cache will be stale. Thread.sleep(20000); request = new Request.Builder() .cacheControl(new CacheControl.Builder().onlyIfCached().build()) .url("http://httpbin.org/cache/20").build(); Response response10 = client.newCall(request).execute(); assertEquals(504, response10.code()); assertEquals("Unsatisfiable Request (only-if-cached)", response10.message()); request = new Request.Builder() .cacheControl(new CacheControl.Builder().maxStale(7, TimeUnit.DAYS).build()) .url("http://httpbin.org/cache/20").build(); Response response11 = client.newCall(request).execute(); response11.close(); assertNotNull(response11.cacheResponse()); assertNull(response11.networkResponse()); } |
Firstly, we create a normal request to the remote HTTP service to fetch the resource and cache the response (response9)
1 2 |
Request request = new Request.Builder().url("http://httpbin.org/cache/20").build(); Response response9 = client.newCall(request).execute(); |
Secondly, after making the current thread sleep in 20 seconds to make sure the cached response (response9) is stale, we create a new request to the same remote HTTP service but this request only accepts the response if it is in the cache.
1 2 3 4 5 |
// Sleep 20 seconds to make sure the cache will be stale. Thread.sleep(20000); request = new Request.Builder() .cacheControl(new CacheControl.Builder().onlyIfCached().build()) .url("http://httpbin.org/cache/20").build(); |
Then we execute the request and a 504 Unsatisfiable Request response will be returned.
1 2 3 |
Response response10 = client.newCall(request).execute(); assertEquals(504, response10.code()); assertEquals("Unsatisfiable Request (only-if-cached)", response10.message()); |
Lastly, we create another request and permit stale cached responses by setting the max-stale directive with the maximum staleness in 7 days:
1 2 3 |
request = new Request.Builder() .cacheControl(new CacheControl.Builder().maxStale(7, TimeUnit.DAYS).build()) .url("http://httpbin.org/cache/20").build(); |
Then we execute the request:
1 |
Response response11 = client.newCall(request).execute(); |
Different with the previous request which returned a 504 Unsatisfiable Request response, executing this request will return the stale response in cache.
1 2 |
assertNotNull(response11.cacheResponse()); assertNull(response11.networkResponse()); |
5. Conclusion
The tutorial has shown us how to cache response with OkHttp and how to optimize those caches for our purposes such as to force a network response, to force a cache response, to permit stale cached responses . For effective using of cache, we firstly should understand about HTTP caching, the cache policies controlled from the server side and then apply and optimize the cache stored at the client side.
The example source code can be found the Github project or can be downloaded by clicking on the link java-okhttp-examples.zip. It is a Maven based project which will easily imported into IDEs such as Eclipse or Intellij.
6. References
Java REST Client Examples Using OkHttp
Basic Authentication with OkHttp Example
WebSocket Client Example with OkHttp