Reputation: 1481
In my android app I'm using retrofit 2 with bundled okhttp. I'm using following code to set the cache
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
File httpCacheDirectory = new File(MyApplication.getInstance().getCacheDir(), "responses");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
httpBuilder.cache(cache);
OkHttpClient httpClient = httpBuilder.build();
Retrofit.Builder builder = new Retrofit.Builder().
baseUrl(ApplicationConstants.BASE_API_URL).
client(httpClient).
addConverterFactory(GsonConverterFactory.create(gson));
The cache headers are being set on the response from the server side. It caches the files just fine and displays them from the cache until the cached file expires.
The issue is when the cache expires, it can no longer be cached again. This no longer caches or replaces the old cached file. I think it should automatically clean old invalid cache files and replace with new response and cache it.
How do I clear the invalid response and cache the new valid response.
I've been trying for almost two days now, no solution. In fact to me it seems I'm doing everything as per documentation. Is there something else that might be wrong.
Here are my response log from okhttp
D/OkHttp: Connection: keep-alive
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Server: Cowboy
D/OkHttp: X-Frame-Options: SAMEORIGIN
D/OkHttp: X-Xss-Protection: 1; mode=block
D/OkHttp: X-Content-Type-Options: nosniff
D/OkHttp: Date: Tue, 02 Aug 2016 17:39:23 GMT
D/OkHttp: X-Pagination: {"total":34,"total_pages":2,"first_page":true,"last_page":false,"prev_page":null,"next_page":2,"out_of_range":false}
D/OkHttp: Cache-Control: max-age=10800, public, no-transform
D/OkHttp: Etag: W/"4dcf69c9456102fd57666a1dff0eec3a"
D/OkHttp: X-Request-Id: 1fb917ac-7f77-4c99-8a3b-20d56af9d441
D/OkHttp: X-Runtime: 0.081711
D/OkHttp: Via: 1.1 vegur
My cache header for json response is below:
Thanks in advance,
Upvotes: 2
Views: 1859
Reputation: 1185
You didn't explicitly mention use of ETags. But I've had this same issue as a result of their use. So it may be related.
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 a 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. The only solution I've come up with is to provide a "no-cache" Cache-Control header when I know the cache has expired and needs a "refresh". This will retrieve the response (assuming it's a 200) and refresh the cache and its expiration.
Here's an approach as an example. The outline:
One drawback to this approach is that we force the refresh using no-cache. This means the server will end up serving the content even if the content is the same as what is already cached. In my case, this was an acceptable trade-off, since the server was processing the request all the same (just to generate the ETag hash). So the conditional if-none-match was only saving the payload transfer and not web server processing. In my case the server processing accounted for nearly all of the performance hit (thus my need to force a cache in the first place).
Here's some code:
<!-- language: kotlin -->
class RestAdapterFactory(private val app: Context) {
private var httpClient: OkHttpClient.Builder? = null
private var builder: Retrofit.Builder? = null
private val MAX_AGE = 60 * 1
private val READ_TIMEOUT = 30.toLong()
private val CACHE_SIZE = 5 * 1024 * 1024.toLong() // 5 MB
private val apiCache: MutableMap<String, DateTime> = HashMap()
private fun getHttpClient(): OkHttpClient.Builder {
if (httpClient == null) {
httpClient = OkHttpClient.Builder()
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.cache(Cache(app.cacheDir, CACHE_SIZE))
.addNetworkInterceptor(getWebApiResponseInterceptor())
.addInterceptor(getConditionalCacheRequestInterceptor())
}
return httpClient!!
}
/***
* Stores url entry with time to expire (to be used in conjunction with [getConditionalCacheRequestInterceptor]
*/
private fun getWebApiResponseInterceptor(): (Interceptor.Chain) -> Response {
return {
val response = it.proceed(it.request())
apiCache[it.request().url().toString()] = DateTime().plusSeconds(MAX_AGE)
response.newBuilder().header("Cache-Control", "max-age=$MAX_AGE").build() // forcing client to cache
}
}
/***
* Checks expiration of url, if url exists and is expired then force a response with no-cache
*/
private fun getConditionalCacheRequestInterceptor(): (Interceptor.Chain) -> Response {
return {
val original = it.request()
val urlExpiration = apiCache[original.url().toString()]
val noCache = urlExpiration == null || urlExpiration < DateTime()
it.proceed(if (noCache) original.newBuilder().header("Cache-Control", "no-cache").build() else original)
}
}
}
Upvotes: 1