Probably requires fancy joining & canceling. Stale-while-revalidate is quite tricky because a single request may yield multiple responses.
+1.
Very useful if this would be natively supported instead of rolling our own implementation :)
I think we can do something quick & simple for stale-while-revalidate: use it to really force a cached response, and hint to the caller whether another request should be performed.
Is there a equivalent to stale-if-error in 2.x; I thought:
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks
connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
Would do the same, but alas not.
Any update on this one, after 1 year? We would like to take advantage of these headers in our app.
@ntarocco this will be difficult. The catch is that implementing this will cause us to create two responses for a single request. We currently have no mechanism for that. As I mentioned, you can simulate this today with a force-cache request and a follow-up.
stale-if-error handling shouldn't be difficult to implement for the local cache.
Taking a quick look through the implementation, my evaluation of implementing stale-while-revalidate is that it could be done by modifying the HttpEngine implementation to serve the non-stale response when readResponse() is called a second time after receiving a stale response with this Cache-Control header value. The asynchronous Call implementation could be modified to read and deliver the response to the callback multiple times until it receives a non-stale response or times out. As for the synchronous API, that could also be modified to include a method which allows the caller to request the non-stale response after receiving the stale one, as @swankjesse previously suggested.
I'd just like to vote for stale-if-error. It's a very common use case, both judging by our needs and by the frequency of similar requests (on SO and various other github issues). Frankly I'm surprised this issue isn't more "popular". Although stale-while-revalidate would also be nice, it's not as prevalent as stale-if-error which we'd like to use in most of our apps.
The related retrofit issue also seems no more active other than a proposed workaround.
Proposed solutions only partially solve the problem. For example, a common solution is to use an interceptor which looks whether network connectivity is available, but does nothing to recover if the server is down or has some other issues - which is exactly what many comments relating to this issue describe. Even the stale-if-error RFC example mentions this case.
It should be possible to implement stale-if-error with am interceptor.
We have implemented stale-if-error as an interceptor and it's been working well for quite a while.
The gist is you have to look for a Warning: 110 (Response is stale) header and if you find it, retry the request (possibly with CacheControl.FORCE_NETWORK) and return the new response if it's successful, otherwise return the original one.
@felipecsl Can you provide a sample for this? I am struggling with it a little. It seems like the max-stale part has an impact on requests while you are online and they requests are returned from cache (even though I would always like to execute the request and never return from cache if the device is online).
I am really hoping that OkHttp will support stale-if-error and stale-while-revalidate in future :-).
public class CacheRevalidationInterceptor implements Interceptor {
private static final String WARNING_RESPONSE_IS_STALE = "110";
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response originalResponse = chain.proceed(request);
if (shouldSkipInterceptor(request, originalResponse)) {
return originalResponse;
}
Request modifiedRequest = removeCacheHeaders(request);
try {
Response retriedResponse = chain.proceed(modifiedRequest);
if (retriedResponse == null || !retriedResponse.isSuccessful()) {
return originalResponse;
}
return retriedResponse;
} catch (IOException e) {
return originalResponse;
}
}
private boolean shouldSkipInterceptor(Request request, Response response) {
// not much we can do in this case
if (response == null) {
return true;
}
List<String> warningHeaders = response.headers("Warning");
for (String warningHeader : warningHeaders) {
// if we can find a warning header saying that this response is stale, we know
// that we can't skip it.
if (warningHeader.startsWith(WARNING_RESPONSE_IS_STALE)) {
return false;
}
}
return true;
}
private Request removeCacheHeaders(Request request) {
Headers modifiedHeaders = request.headers()
.newBuilder()
.removeAll("Cache-Control")
.build();
return request.newBuilder()
.headers(modifiedHeaders)
.build();
}
}
In this case, I'm retrying the request without the Cache-Control header, which makes it go to the network and retry for an updated version. If that fails for whatever reason, we just return the stale version.
Hope this helps
Here is my WIP attempt at stale-if-error.
public class StaleIfErrorAppInterceptor implements Interceptor {
private static final String TAG = "StaleIfErrorAppIntercep";
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Response response = null;
Request request = chain.request();
// first try the regular (network) request, guard with try-catch
// so we can retry with force-cache below
try {
response = chain.proceed(request);
// return the original response only if it succeeds
if (response.isSuccessful()) {
return response;
} else {
throw new IOException("Response unsuccessful, wrong response code: "
+ response.code());
}
} catch (Exception e) {
Log.d(TAG, String.format("Original request error: %s [%s]",
request.url(), e.getMessage()));
}
if (response == null || !response.isSuccessful()) {
Request newRequest = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
try {
response = chain.proceed(newRequest);
} catch (Exception e) {
Log.d(TAG, String.format("Force cache request error: %s [%s]",
newRequest.url(), e.getMessage()));
throw e;
}
}
return response;
}
}
The IOE throw and second try-catch might be excessive but as I said, this is still WIP/testing version. What is important though, if you want to force this for any response, regardless of the server specified Cache-Control, an additional network interceptor is necessary to remove it. This will get rid of directives such as must-revalidate, no-cache, no-store - I don't think you can circumvent those by using only request headers. Here is an example:
public class RemoveCacheControlNetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
return response.newBuilder()
.removeHeader("Cache-Control")
.removeHeader("Pragma")
.build();
}
}
Yes, this is dangerous and goes "against" the server but there are valid use-cases. You could also build a mechanism which does this header-purge selectively (e.g. by setting a custom header when building the request and reading it in the interceptor).
I have simple use case: Use network response when success. Else use cached response.
But the problem is that when network response is error, cache is also written with that response (for the response codes in CacheStrategy.
I have tried with solutions mentioned above but couldn't make it work. One of the suggestion I read is to do FORCE_CACHE in the Interceptor when networkResponse is not successful.
Since the networkResponse overrides cache with error, next time when you request (and server still returns error), the cache will have error.
Below is my current snippet. I need to add logic for returning cached value when networkResponse is error. Any suggestion will be greatly helpful.
private void setup() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(REWRITE_CACHE_CONTROL);
File httpCacheDirectory = new File(context.getCacheDir(), "responses");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024); // 10 MB
builder.cache(cache);
}
private static final Interceptor REWRITE_CACHE_CONTROL = new Interceptor() {
@Override
public okhttp3.Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();
if (!hasAConnection()) {
request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
}
return chain.proceed(request);
}
};
We implemented this interceptor as a resuable library in https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/ based on the approach outlined here. We have used it in production, and it works well.
The interceptor is a nice solution & I鈥檝e linked it from our wiki. Nothing further planned here!
We implemented this interceptor as a resuable library in https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/ based on the approach outlined here. We have used it in production, and it works well.
Doesn't work for me at all, sadly :-/
We implemented this interceptor as a resuable library in https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/ based on the approach outlined here. We have used it in production, and it works well.
Doesn't work for me at all, sadly :-/
Please file a bug in that project with reproducible steps.
BTW, it still works for us.
From what I see, this interceptor only solves the stale-if-error part, not the stale-while-revalidate. Is there a way to achieve later?
Also the wiki link is dead :/
@matejdro I think an application interceptor of similar complexity to the stale-if-error interceptor should work https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/blob/master/src/main/java/com/peel/okhttp3/staleiferror/StaleIfErrorInterceptor.java
Perhaps something you could implement and contribute to that project?
stale-while-revalidate is completely different from stale-if-error and I do not see how it could be implemented with an interceptor - issue is that OkHttp would somehow have to return more than one response (first cached response and then new server response if cached one was stale).
@matejdro Good point. So probably better handled externally by checking the response (if cached and stale). okhttp-dnsoverhttps has an example of forcing a cache response. But the comments suggest that it fails on a stale result :(
So you might need to play around with cache headers. The docs give some hints here https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/#force-a-cache-response
You could try that and then optionally fork a background request with enqueue to refresh if it's older than you want.
I think short answer is that we don't currently support this in OkHttp, but ultimately you can probably get it working if you are motivated enough to dive in.
We've also improved the cache transparency through event listener API recently - so consider using this if you decide to tackle it.
Most helpful comment
We implemented this interceptor as a resuable library in https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/ based on the approach outlined here. We have used it in production, and it works well.