158

I'm trying to use Retrofit & OKHttp to cache HTTP responses. I followed this gist and, ended up with this code:

File httpCacheDirectory = new File(context.getCacheDir(), "responses");

HttpResponseCache httpResponseCache = null;
try {
     httpResponseCache = new HttpResponseCache(httpCacheDirectory, 10 * 1024 * 1024);
} catch (IOException e) {
     Log.e("Retrofit", "Could not create http cache", e);
}

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setResponseCache(httpResponseCache);

api = new RestAdapter.Builder()
          .setEndpoint(API_URL)
          .setLogLevel(RestAdapter.LogLevel.FULL)
          .setClient(new OkClient(okHttpClient))
          .build()
          .create(MyApi.class);

And this is MyApi with the Cache-Control headers

public interface MyApi {
   @Headers("Cache-Control: public, max-age=640000, s-maxage=640000 , max-stale=2419200")
   @GET("/api/v1/person/1/")
   void requestPerson(
           Callback<Person> callback
   );

First I request online and check the cache files. The correct JSON response and headers are there. But when I try to request offline, I always get RetrofitError UnknownHostException. Is there anything else I should do to make Retrofit read the response from cache?

EDIT: Since OKHttp 2.0.x HttpResponseCache is Cache, setResponseCache is setCache

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
osrl
  • 8,168
  • 8
  • 36
  • 57
  • 1
    Is the server you're calling responding with an appropriate Cache-Control header? – Hassan Ibraheem May 03 '14 at 19:58
  • it returns this `Cache-Control: s-maxage=1209600, max-age=1209600` I don't know if it's enough. – osrl May 04 '14 at 19:02
  • Seems like the `public` keyword was needed to be in response header to make it work offline. But, these headers doesn't let OkClient to use network when there is available. Is there anyway to set cache policy/strategy to use network when available? – osrl May 06 '14 at 15:54
  • I'm not sure whether you can do that in the same request. You can check the relevant [CacheControl](https://github.com/square/okhttp/blob/master/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java) class, and the [Cache-Control](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) headers. If there's no such behavior, I would probably opt for making two requests, a cached only request (only-if-cached), followed by a network (max-age=0) one. – Hassan Ibraheem May 06 '14 at 16:35
  • that was the first thing came to my mind. I spent days in that CacheControl and [CacheStrategy](https://github.com/square/okhttp/blob/276908f5322af10ddad0bb4ed024d2edf8939731/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java) classes. But two requests idea didn't made much sense. If `max-stale + max-age` is passed, it does request from network. But I want to set max-stale a week. This makes it read response from cache even if there is network available. – osrl May 06 '14 at 17:59
  • Isn't s-maxage for the server side only, not clients? – McNinja Oct 16 '14 at 15:56
  • Also checkout headers from server like here: http://stackoverflow.com/questions/31321963/how-retrofit-with-okhttp-use-cache-data-when-offline/31606496#31606496 – Gelldur Jul 24 '15 at 09:11
  • rtm: https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/#force-a-cache-response – ccpizza Jul 22 '20 at 19:33

7 Answers7

198

Edit for Retrofit 2.x:

OkHttp Interceptor is the right way to access cache when offline:

1) Create Interceptor:

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        if (Utils.isNetworkAvailable(context)) {
            int maxAge = 60; // read from cache for 1 minute
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
    }

2) Setup client:

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);

//setup cache
File httpCacheDirectory = new File(context.getCacheDir(), "responses");
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);

//add cache to the client
client.setCache(cache);

3) Add client to retrofit

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

Also check @kosiara - Bartosz Kosarzycki's answer. You may need to remove some header from the response.


OKHttp 2.0.x (Check the original answer):

Since OKHttp 2.0.x HttpResponseCache is Cache, setResponseCache is setCache. So you should setCache like this:

        File httpCacheDirectory = new File(context.getCacheDir(), "responses");

        Cache cache = null;
        try {
            cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
        } catch (IOException e) {
            Log.e("OKHttp", "Could not create http cache", e);
        }

        OkHttpClient okHttpClient = new OkHttpClient();
        if (cache != null) {
            okHttpClient.setCache(cache);
        }
        String hostURL = context.getString(R.string.host_url);

        api = new RestAdapter.Builder()
                .setEndpoint(hostURL)
                .setClient(new OkClient(okHttpClient))
                .setRequestInterceptor(/*rest of the answer here */)
                .build()
                .create(MyApi.class);

Original Answer:

It turns out that server response must have Cache-Control: public to make OkClient to read from cache.

Also If you want to request from network when available, you should add Cache-Control: max-age=0 request header. This answer shows how to do it parameterized. This is how I used it:

RestAdapter.Builder builder= new RestAdapter.Builder()
   .setRequestInterceptor(new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            request.addHeader("Accept", "application/json;versions=1");
            if (MyApplicationUtils.isNetworkAvailable(context)) {
                int maxAge = 60; // read from cache for 1 minute
                request.addHeader("Cache-Control", "public, max-age=" + maxAge);
            } else {
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                request.addHeader("Cache-Control", 
                    "public, only-if-cached, max-stale=" + maxStale);
            }
        }
});
Community
  • 1
  • 1
osrl
  • 8,168
  • 8
  • 36
  • 57
  • (I was wondering why this didn't work; turned out I forgot to set the actual cache for OkHttpClient to use. See the code in the question or in [this answer](http://stackoverflow.com/a/24514535/56285).) – Jonik Mar 10 '15 at 15:13
  • 2
    Just a word of advice: `HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...)`. – Henrique de Sousa May 28 '15 at 11:46
  • Yes you are right, I've already added it to the question. I should also add it to the answer – osrl May 29 '15 at 07:16
  • 2
    I don't get interceptor called when network is not available. I am not sure how can the condition when network is not available will hit. Am I missing something here? – Androidme Apr 12 '16 at 11:15
  • Why do you need to explicitly set the directive `public`? Is it private by default? – Etienne Lawlor Jun 16 '16 at 02:03
  • @toobsco42 which one? Header? – osrl Jun 16 '16 at 07:45
  • @osrl Ya for the `Cache-Control` header. – Etienne Lawlor Jun 16 '16 at 08:15
  • I'm not really experienced with the headers. It was a trial and error solution for me. I said in a comment: "Seems like the `public` keyword was needed to be in response header to make it work offline." So it was private by default. It would be great if someone explained it. – osrl Jun 16 '16 at 08:40
  • 2
    is the `if (Utils.isNetworkAvailable(context))` correct or is it supposed to be reversed i.e. `if (!Utils.isNetworkAvailable(context))` ? – ericn Jul 22 '16 at 06:35
  • 2
    I'm using Retrofit 2.1.0 and when the phone is offline, `public okhttp3.Response intercept(Chain chain) throws IOException` is never called, it's only called when I'm online – ericn Jul 22 '16 at 07:10
  • I have a rails api and it already sets the cache-control headers for the response. Still it's not working. I'm using retrofit 2.1.0 – lightsaber Jul 30 '16 at 14:39
  • Sorry, it is working. Storage permission was toggled to off. – lightsaber Jul 30 '16 at 15:13
  • 1
    The data is only available offline here for the time of 'max-age', after 60 seconds it cannot be loaded. @StarWars any idea? – Boy Aug 11 '16 at 11:59
  • @Boy Actually still not working. It was working on one+ but not on nexus 5. I guess it's issue with nexus devices. – lightsaber Aug 11 '16 at 14:59
  • @StarWars I gave up and started caching the data itself instead – Boy Aug 15 '16 at 07:00
  • I've spent a lot of time because I was sending a date as a parameter and because of that the request couldn't be cached. Even though it may seem stupid, I though it could be useful if I wrote it in an answer that works, so here it is. – Oriol Nov 08 '16 at 07:15
  • @osrl Why we are checking internet connection while response interception? Thanks,though this solution is working for me but I am not getting the flow. – Rohit Bandil Jan 20 '17 at 13:00
  • @RohitBandil This is a good question. You might be right about intercepting the request instead of response. I've added headers to the request on the original answer. I really don't remember why I did that. – osrl Jan 23 '17 at 12:33
  • @osrl what is the purpose to add a header in a request when the internet is not available in your original answer.Anyways it will throw you a network not available exception. – Rohit Bandil Feb 01 '17 at 11:20
  • I am having the same issue as @ericn described. Using the above code for the interceptor on Retrofit 2.3.0 gives me a `java.net.ConnectException` when i'm offline. The exception is already thrown at the line `Response originalResponse = chain.proceed(chain.request());`. Hence I don't even get to the check whether the device is online or offline. Anyone knows how to fix this? – ice_chrysler Jan 22 '19 at 16:27
  • Update to my comment. Using the the lib posted in an anser below by @Nicolas Cornette: github.com/ncornette/OkCacheControl it is working and the cache is used when i'm offline. Thanks for that. But still i would be interested in why above interceptor doesn't work for me? – ice_chrysler Jan 22 '19 at 16:49
32

All of the anwsers above did not work for me. I tried to implement offline cache in retrofit 2.0.0-beta2. I added an interceptor using okHttpClient.networkInterceptors() method but received java.net.UnknownHostException when I tried to use the cache offline. It turned out that I had to add okHttpClient.interceptors() as well.

The problem was that cache wasn't written to flash storage because the server returned Pragma:no-cache which prevents OkHttp from storing the response. Offline cache didn't work even after modifying request header values. After some trial-and-error I got the cache to work without modifying the backend side by removing pragma from reponse instead of the request - response.newBuilder().removeHeader("Pragma");

Retrofit: 2.0.0-beta2; OkHttp: 2.5.0

OkHttpClient okHttpClient = createCachedClient(context);
Retrofit retrofit = new Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build();
service = retrofit.create(RestDataResource.class);

...

private OkHttpClient createCachedClient(final Context context) {
    File httpCacheDirectory = new File(context.getCacheDir(), "cache_file");

    Cache cache = new Cache(httpCacheDirectory, 20 * 1024 * 1024);
    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setCache(cache);
    okHttpClient.interceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    okHttpClient.networkInterceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    return okHttpClient;
}

...

public interface RestDataResource {

    @GET("rest-data") 
    Call<List<RestItem>> getRestData();

}
JJD
  • 50,076
  • 60
  • 203
  • 339
22

My solution:

private BackendService() {

    httpCacheDirectory = new File(context.getCacheDir(),  "responses");
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(httpCacheDirectory, cacheSize);

    httpClient = new OkHttpClient.Builder()
            .addNetworkInterceptor(REWRITE_RESPONSE_INTERCEPTOR)
            .addInterceptor(OFFLINE_INTERCEPTOR)
            .cache(cache)
            .build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.backend.com")
            .client(httpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    backendApi = retrofit.create(BackendApi.class);
}

private static final Interceptor REWRITE_RESPONSE_INTERCEPTOR = chain -> {
    Response originalResponse = chain.proceed(chain.request());
    String cacheControl = originalResponse.header("Cache-Control");

    if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
            cacheControl.contains("must-revalidate") || cacheControl.contains("max-age=0")) {
        return originalResponse.newBuilder()
                .header("Cache-Control", "public, max-age=" + 10)
                .build();
    } else {
        return originalResponse;
    }
};

private static final Interceptor OFFLINE_INTERCEPTOR = chain -> {
    Request request = chain.request();

    if (!isOnline()) {
        Log.d(TAG, "rewriting request");

        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
        request = request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .build();
    }

    return chain.proceed(request);
};

public static boolean isOnline() {
    ConnectivityManager cm = (ConnectivityManager) MyApplication.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = cm.getActiveNetworkInfo();
    return netInfo != null && netInfo.isConnectedOrConnecting();
}
Arkadiusz Konior
  • 1,139
  • 13
  • 12
7

The answer is YES, based on the above answers, I started writing unit tests to verify all possible use cases :

  • Use cache when offline
  • Use cached response first until expired, then network
  • Use network first then cache for some requests
  • Do not store in cache for some responses

I built a small helper lib to configure OKHttp cache easily, you can see the related unittest here on Github : https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java

Unittest that demonstrates the use of cache when offline :

@Test
public void test_USE_CACHE_WHEN_OFFLINE() throws Exception {
    //given
    givenResponseInCache("Expired Response in cache", -5, MINUTES);
    given(networkMonitor.isOnline()).willReturn(false);

    //when
    //This response is only used to not block when test fails
    mockWebServer.enqueue(new MockResponse().setResponseCode(404));
    Response response = getResponse();

    //then
    then(response.body().string()).isEqualTo("Expired Response in cache");
    then(cache.hitCount()).isEqualTo(1);
}

As you can see, cache can be used even if it has expired. Hope it will help.

Nicolas Cornette
  • 772
  • 7
  • 11
6

building on @kosiara-bartosz-kasarzycki's answer, I created a sample project that properly loads from memory->disk->network using retrofit, okhttp, rxjava and guava. https://github.com/digitalbuddha/StoreDemo

Community
  • 1
  • 1
FriendlyMikhail
  • 2,857
  • 23
  • 39
2

Cache with Retrofit2 and OkHTTP3:

OkHttpClient client = new OkHttpClient
  .Builder()
  .cache(new Cache(App.sApp.getCacheDir(), 10 * 1024 * 1024)) // 10 MB
  .addInterceptor(new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      if (NetworkUtils.isNetworkAvailable()) {
        request = request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build();
      } else {
        request = request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build();
      }
      return chain.proceed(request);
    }
  })
  .build();

NetworkUtils.isNetworkAvailable() static method:

public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

Then just add client to the retrofit builder:

Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

Original source: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

1

Attention! OkHttp build in cache only work for GET method(refer to above solution). If you want to cache POST request, you must implement yourself. enter image description here

聂超群
  • 1,659
  • 6
  • 14