16

Is there a proper explanation on how to add caching and ETAG/If-None-Match support to Retrofit+OkHttp? I'm struggling to add Etag support on 2 projects, and at first I suspected that there might be an issue with HTTP headers, another project has everything set correctly and caching still doesn't work as expected.

Following are my attempts to make it work. Results show that caching seems to be working within the same instance of the application, but as soon as I restart - everything loads long again. Also, in my logs I didn't see If-None-Match being added to a request, so I assume that server isn't aware of ETag and still recalculates the response completely.

Here are some code samples:

public class RetrofitHttpClient extends UrlConnectionClient
{

    private OkUrlFactory generateDefaultOkUrlFactory()
    {
        OkHttpClient client = new com.squareup.okhttp.OkHttpClient();

        try
        {
            Cache responseCache = new Cache(baseContext.getCacheDir(), SIZE_OF_CACHE);
            client.setCache(responseCache);
        }
        catch (Exception e)
        {
            Logger.log(this, e, "Unable to set http cache");
        }

        client.setConnectTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
        client.setReadTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS);
        return new OkUrlFactory(client);
    }

    private final OkUrlFactory factory;

    public RetrofitHttpClient()
    {
        factory = generateDefaultOkUrlFactory();
    }

    @Override
    protected HttpURLConnection openConnection(retrofit.client.Request request) throws IOException
    {
        return factory.open(new URL(request.getUrl()));
    }
}

Rest adapter is then created with FULL log level and a custom tag:

restAdapter = new RestAdapter.Builder()
        .setClient(new RetrofitHttpClient())
        .setEndpoint(Config.BASE_URL)
        .setRequestInterceptor(new SignatureSetter())
        .setConverter(new JacksonConverter(JsonHelper.getObjectMapper()))
        .setLogLevel(RestAdapter.LogLevel.FULL)
        .setLog(new AndroidLog("=NETWORK="))
        .build();

I have a long request on the first screen of the app for testing. When I open the app - it takes 7 seconds to complete the request. If I pause and resume the app - same request takes 250ms, clearly hitting the cache. If I close the app completely and restart - it again takes 7 seconds.

UPDATE: As was suggested, I have used a custom Retrofit build and attached a LoggingInterceptor. Here's what I'm getting.

Received response for *** in 449,3ms
Date: Wed, 07 Jan 2015 09:02:23 GMT
Server: Apache
X-Powered-By: PHP/5.4.31
Access-Control-Allow-Credentials: true
Pragma:
Cache-Control: public, max-age=3600
X-Frame-Options: SAMEORIGIN
Etag: "hLxLRYztkinJAB453nRV7ncBSuU=-gzip"
Last-Modified: Wed, 24 Dec 2014 13:09:04 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1420621288104
OkHttp-Received-Millis: 1420621288554


Sending request **** on Connection{****:80, proxy=DIRECT@ hostAddress=**** cipherSuite=none protocol=http/1.1}
Accept: application/json;
Host: ****
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/2.2.0

Response is equal to described above

As you can see, no If-None-Match header is present in the next request.

Nisse Engström
  • 4,738
  • 23
  • 27
  • 42
AAverin
  • 3,014
  • 3
  • 27
  • 32
  • Does the server response have `Cache-Control` properly implemented? – Andrei Catinean Dec 28 '14 at 00:42
  • Yes. as I mentioned I have 2 projects. On 1 project server has Cache-Control: max-age=0, private, must-revalidate. That doesn't look correct, I know. But another server has correct Cache-Control settings that allow caching for some time. – AAverin Dec 28 '14 at 09:00
  • Are you fully consuming the response body? If you don't read the entire response body, it will not be cached. (You also need to call `close()` at the end.) – Jesse Wilson Jan 08 '15 at 21:55
  • Server responds with a JSON, I use a JacksonConverter to make a model of it. So I assume that conversion should consume the whole body to make a valid model, and this stuff is retrofit-internal – AAverin Jan 09 '15 at 06:40

3 Answers3

6

I see this question keeps getting attention and as soon as there is no real answer I can pick from - I'm am providing my investigation on the topic and closing the thread for now.

The end result of investigation and some discussions in the retrofit and okhttp threads on GitHub was that there was supposedly an issue in OkHttp that could prevent If-None-Match tag being set for the outgoing requests.

The issue was supposed to be fixed in OkHttp 2.3, and I'm using 'supposed' here because I didn't yet test if it really works. The testing was difficult because I was using Retrofit, and Retrofit itself had to be updated to use the new version of OkHttp and add some new Interceptors support to be able to debug all headers that are set by OkHttp. Related thread is here: https://github.com/square/okhttp/issues/831

I'm not sure if Retrofit was updated after that. Hopefully it was, so there is a good chance that issue is already fixed and Etag should properly work - just make sure you have latest versions of Retrofit and OkHttp.

I will try to test everything myself once I have time.

AAverin
  • 3,014
  • 3
  • 27
  • 32
1

Using OkHttp interceptors will help you to diagnose the headers coming in & out of your application. The interceptors doc gives a code example of an interceptor that logs request & response headers on the network. You can use this as-is.

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

To hook it up to Retrofit, you'll need to get a pre-release snapshot of Retrofit. As of January, 2015, the currently-shipping versions of Retrofit don't participate in OkHttp's interceptors. There will be a release shortly that does, but it's not ready yet.

Jesse Wilson
  • 39,078
  • 8
  • 121
  • 128
  • Retrofit logging set to FULL displays headers of incoming/outgoing requests, but I don't see If-None-Match there. It doesn't display all headers? If it so, why? – AAverin Jan 04 '15 at 10:02
  • 1
    Retrofit doesn't know about the headers that [get added](https://github.com/square/okhttp/wiki/Calls) by OkHttp, including the `Cache-Control` header. – Jesse Wilson Jan 05 '15 at 04:09
  • Same problem with @AAverin, dont see If-None-Match on retrofit log. Thanks jesse-wilson, wait retrofit release. – Crossle Song Jan 05 '15 at 08:55
  • @AAverin when use ETag, your server return 304? – Crossle Song Jan 05 '15 at 09:01
  • @CrossleSong, if I implement Etag manually and send it manually as a separate parameter - yes, server responds with 304 and everything works. If I leave Etag for okHttp to resolve - I don't see that I really hit the cache. – AAverin Jan 05 '15 at 09:06
  • 1
    @AAvern I use okhttp cache, not manually add ETag, but still return 200, not 304. I foud /data/data/package_name/cache/HttpCache include cache, but, not return 304 – Crossle Song Jan 06 '15 at 02:18
  • @JesseWilson, I have followed your suggestion and inserted a LogginInterceptor into a Retrofit client. I have attached the logs, no If-None-Match is present – AAverin Jan 07 '15 at 09:06
  • Is it registered as a network interceptor? Ie. do the logs have a non-null connection? What headers get returned by the server? – Jesse Wilson Jan 07 '15 at 14:21
  • Yes, I have registered id as a networkInterceptor. All logs returned by the server are in updated post on the top. – AAverin Jan 08 '15 at 09:17
  • 1
    One downside to OkHttp's support for ETags is that OkHttp never "refreshes" the cache's expiration. So once it is expired, it will never use the cache until it is updated. This will only occur if the resource has been updated (etags are different and 200 response is returned vs. 304).. This means that the OkHttp client will continue to go to the network for each subsequent request as long as it continues to get a 304 response. The HTTP spec is vague on how clients should handle this scenario, so I don't think it's a bug. But it does defeat the purpose of a cache if I keep hitting the network. – Steven Pena Aug 27 '16 at 20:43
0

I was having a similar problem: OkHttp not hitting cache ever, even when the server was sending same ETAG.

My issue was SIZE_OF_CACHE. I was defining a very small size.

Try to increase it (something like 10 * 1024 * 1024 works for me) Also you can explore /data/data//files/cache to see if there is actually something stored there

lgvalle
  • 3,712
  • 1
  • 19
  • 14
  • SIZE_OF_CACHE = 10 * 1024 * 1024 - that's what I have now, and it still doesn't work as expected. I can try increasing it, but most likely that isn't the issue – AAverin Dec 30 '14 at 18:34