/** Returns true if {@code response} can be stored to later serve another request. */ public static boolean isCacheable(Response response, Request request) { // Always go to network for uncacheable response codes (RFC 7231 section 6.1), // This implementation doesn't support caching partial content. switch (response.code()) { case HTTP_OK: case HTTP_NOT_AUTHORITATIVE: case HTTP_NO_CONTENT: case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_NOT_FOUND: case HTTP_BAD_METHOD: case HTTP_GONE: case HTTP_REQ_TOO_LONG: case HTTP_NOT_IMPLEMENTED: case HTTP_PERM_REDIRECT: // These codes can be cached unless headers forbid it. break; case HTTP_MOVED_TEMP: case HTTP_TEMP_REDIRECT: // These codes can only be cached with the right response headers. // http://tools.ietf.org/html/rfc7234#section-3 // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage. if (response.header("Expires") != null || response.cacheControl().maxAgeSeconds() != -1 || response.cacheControl().isPublic() || response.cacheControl().isPrivate()) { break; } // Fall-through. default: // All other codes cannot be cached. return false; } // A 'no-store' directive on request or response prevents the response from being cached. return !response.cacheControl().noStore() && !request.cacheControl().noStore(); }
/** * Returns the number of milliseconds that the response was fresh for, starting from the served * date. */ private long computeFreshnessLifetime() { CacheControl responseCaching = cacheResponse.cacheControl(); if (responseCaching.maxAgeSeconds() != -1) { return SECONDS.toMillis(responseCaching.maxAgeSeconds()); } else if (expires != null) { long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; long delta = expires.getTime() - servedMillis; return delta > 0 ? delta : 0; } else if (lastModified != null && cacheResponse.request().url().getQuery() == null) { // As recommended by the HTTP RFC and implemented in Firefox, the // max age of a document should be defaulted to 10% of the // document's age at the time it was served. Default expiration // dates aren't used for URIs containing a query. long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; long delta = servedMillis - lastModified.getTime(); return delta > 0 ? (delta / 10) : 0; } return 0; }
/** * Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a * cached response older than 24 hours, we are required to attach a warning. */ private boolean isFreshnessLifetimeHeuristic() { return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null; }
/** Returns a strategy to use assuming the request can use the network. */ private CacheStrategy getCandidate() { // No cached response. if (cacheResponse == null) { return new CacheStrategy(request, null); } // Drop the cached response if it's missing a required handshake. if (request.isHttps() && cacheResponse.handshake() == null) { return new CacheStrategy(request, null); } // If this response shouldn't have been stored, it should never be used // as a response source. This check should be redundant as long as the // persistence store is well-behaved and the rules are constant. if (!isCacheable(cacheResponse, request)) { return new CacheStrategy(request, null); } CacheControl requestCaching = request.cacheControl(); if (requestCaching.noCache() || hasConditions(request)) { return new CacheStrategy(request, null); } long ageMillis = cacheResponseAge(); long freshMillis = computeFreshnessLifetime(); if (requestCaching.maxAgeSeconds() != -1) { freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } long minFreshMillis = 0; if (requestCaching.minFreshSeconds() != -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); } long maxStaleMillis = 0; CacheControl responseCaching = cacheResponse.cacheControl(); if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); } if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { Response.Builder builder = cacheResponse.newBuilder(); if (ageMillis + minFreshMillis >= freshMillis) { builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\""); } long oneDayMillis = 24 * 60 * 60 * 1000L; if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\""); } return new CacheStrategy(null, builder.build()); } Request.Builder conditionalRequestBuilder = request.newBuilder(); if (etag != null) { conditionalRequestBuilder.header("If-None-Match", etag); } else if (lastModified != null) { conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString); } else if (servedDate != null) { conditionalRequestBuilder.header("If-Modified-Since", servedDateString); } Request conditionalRequest = conditionalRequestBuilder.build(); return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest, cacheResponse) : new CacheStrategy(conditionalRequest, null); }