okhttpのCacheControl.Builder.maxStale()

okhttpのCacheControl.Builder.maxStale()の挙動を勘違いしてた。

たとえばこんなコードを書いたとする。

val CACHE_CONTROL = CacheControl.Builder()
	.maxAge(5, TimeUnit.MINUTES)
	.maxStale( Integer.MAX_VALUE, TimeUnit.SECONDS)
	.build()

val call = okhttpClient.newCall(
	Request.Builder()
	.cacheControl(CACHE_CONTROL)
	.url(url)
)


maxStale()で指定した値がどのように利用されるか追ってみる。

  • maxStale()で指定した値は CacheControl.maxStaleSeconds() で参照できる。
  • okhttp内部でmaxStaleSeconds()を呼び出しているのはCacheStrategy.Factory.getCandidate() だ。
  • getCandidate() は CacheInterceptor.intercept() から CacheStrategy.Factory.get() 経由で呼び出される。
  • RealCall.getResponseWithInterceptorChain() が CacheInterceptor を生成する。

CacheStrategy.Factory.getCandidate()は以下の条件が揃うとネットワークアクセスなしでキャッシュレスポンスを返す。

  • リクエストのCacheControlのmustRevalidate() が偽
  • リクエストのCacheControlのnoCache()が偽
  • リクエストのCacheControlのmaxStaleSeconds()が-1ではない
  • ageMillis + minFreshMillis < freshMillis + maxStaleMillis

つまり冒頭のコードは、意図せずに「一度キャッシュしたら Integer.MAX_VALUE 秒の間はネットワークアクセスなしでそのキャッシュを使う」という指定になるのだった。

やりたかったことは「ネットワークアクセスを行いエラーになったらキャッシュ済みのレスポンスを利用する」だったのだが、これを行う方法は

  • 事前にCacheControl.FORCE_CACHEを使ってキャッシュ済みレスポンス(A)を読んでおく
  • maxStale()なしでネットワークアクセスを行う
  • ネットワークアクセスに失敗したら(A)を利用する

という手順のようだった。