Source code analysis, if reprinted, please specify the author: Yuloran (t.cn/EGU6c76)

preface

I’m working on an open source project that needs Http caching. Since the Http client used for the project is OkHttp, you need to understand how to use OkHttp to implement Http cache control. Very ashamed, this is not quite familiar with, so CV to the net. Although I know many blogs on the Internet are not reliable, but I did not expect to fall into the pit.

The wrong sample

I’m not going to name names, there’s a lot on the Internet:

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

        if(! NetworkUtil.isNetworkConnected()) { request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build(); } Response.Builder builder = chain.proceed(request).newBuilder();if (NetworkUtil.isNetworkConnected())
        {
            // if there is a network, no cache is available and the maximum storage duration is 1min
            builder.header("Cache-Control"."public, max-age=60").removeHeader("Pragma");
        } else
        {
            // If there is no network, set the timeout to 1 week
            long maxStale = 60 * 60 * 24 * 7;
            builder.header("Cache-Control"."public, only-if-cached, max-stale=" + maxStale).removeHeader("Pragma");
        }
        returnbuilder.build(); }}/ / to omit...
builder.addNetworkInterceptor(new CacheControlInterceptor());
Copy the code

The result of this code is as follows: after the request is successful, the network is disconnected and the page is opened again. The data can be seen within 1min, and the data disappears after 1min.

The reason for the error

After reviewing the source code for the OKHttp interceptor call and Http cache-control, I found that none of the above code is correct, i.e. the logic is completely wrong:

  1. If there is no network, changing the request header to force caching logic should be placed in an addInterceptor instead of an addNetworkInterceptor. When there is no network, the OkHttp ConnectInterceptor throws UnKnownHostException, which terminates the execution of subsequent interceptors. NetworkInterceptors come just after ConnectInterceptor;

  2. For OkHttp, even if the server does not set the cache-control response header, the client does not need to set it. After the caching function of OkHttpClient is enabled, the response packets of GET requests are automatically cached. To disable caching, add @headers (” cache-control: no-store”) to the interface.

  3. Only -if-cached, max-stale are attributes of the request header, not the response header.

Error proof

Cut straight to the point:

RealCall::execute()

  @Override public Response execute(a) throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      // Initiate a request and get a response
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this); }}Copy the code

RealCall::getResponseWithInterceptorChain()

  Response getResponseWithInterceptorChain(a) throws IOException {
    // Build a full stack of interceptors.
    // Create an array and add all interceptors to it. Because it is an array, it can only be executed in the order in which the interceptor adds it
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors()); // 1. Common interceptor
    interceptors.add(retryAndFollowUpInterceptor); // 2. Connection retry interceptor
    interceptors.add(new BridgeInterceptor(client.cookieJar())); // 3. Request header, response header reprocessing interceptor
    interceptors.add(new CacheInterceptor(client.internalCache())); // 4. Cache saves and reads interceptors
    interceptors.add(new ConnectInterceptor(client)); // 5. Create connection interceptor
    if(! forWebSocket) { interceptors.addAll(client.networkInterceptors());// 6. Network interceptor
    }
    interceptors.add(new CallServerInterceptor(forWebSocket)); // 7. Interface request interceptor

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null.null.null.0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
Copy the code

As can be seen from the source code, all interceptors are stored in the same array, then create a chain, and store the array into the chain. The chain is the header that starts the entire interceptor execution chain. The specific process is as follows:

So why does it not work to change the request header to FORCE_CACHE in a network interceptor? Because the ConnectInterceptor throws UnKnownHostException when there is no network, the network interceptor will not be executed:

In terms of request and response headers, how cache-control is set correctly is described in Http cache-Control.

The correct sample

When no network is available, caching is mandatory:

1. Create a request header interceptor

public class RequestHeadersInterceptor implements Interceptor
{
    private static final String TAG = "RequestHeadersInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Logger.debug(TAG, "RequestHeadersInterceptor.");
        Request request = chain.request();
        Request.Builder builder = request.newBuilder();
        // builder.header("Content-Type", "application/json; charset=UTF-8")
        // .header("Accept-Charset", "UTF-8");
        if(! NetworkService.getInstance().getNetworkInfo().isConnected()) {// If no network is available, caching is mandatory
            Logger.debug(TAG, "network unavailable, force cache.");
            builder.cacheControl(CacheControl.FORCE_CACHE);
        }
        returnchain.proceed(builder.build()); }}Copy the code

NetworkService is a network connection detector I wrote, based on API 21, need to be available: click me

2. Add a request header interceptor

// Cache size is 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addInterceptor(newRequestHeadersInterceptor()); .Copy the code

Tamper with the server response header

In general, the client should not modify the response header. It is up to the server brothers to determine which caching policy the client uses. Only in special cases, additional client configuration is required. For example, the third-party server interface is invoked, and its cache policy does not meet the requirements of the client. Here’s a simple example:

1. Create a response header interceptor

public class CacheControlInterceptor implements Interceptor
{
    private static final String TAG = "CacheControlInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Logger.debug(TAG, "CacheControlInterceptor.");
        Response response = chain.proceed(chain.request());
        String cacheControl = response.header("Cache-Control");
        if (StringUtil.isEmpty(cacheControl))
        {
            Logger.debug(TAG, "'Cache-Control' not set by the backend, add it ourselves.");
            return response.newBuilder().removeHeader("Pragma").header("Cache-Control"."public, max-age=60").build();
        }
        returnresponse; }}Copy the code

2. Add a response header interceptor

// Cache size is 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addNetworkInterceptor(newCacheControlInterceptor ()); .Copy the code

conclusion

The essence of request and response is that different hosts use their RESPECTIVE IP addresses and port numbers to send information to each other through the Socket programming interface. In order to constrain the data exchange format, the Http protocol was created. Because Http is plaintext transmission, Https protocol is developed for transmission security. Since it is an agreement, it will only take effect if both parties abide by it. Therefore, in project development, we often need to conduct interface coordination with the server brother to ensure that the contract is correctly implemented. OkHttp plays a similar role to a browser in that it encapsulates requests and responses in a user-friendly form, supports error reconnection, packet caching, and other mechanisms, but the browser is also responsible for web page rendering.

This article ostensibly describes how to implement cache control using OkHttp, but in fact explains the execution mechanism of OkHttp request and response. The general rules are clear, and implementing other features using OKHttp should not be a problem now. For example, to implement an encryption and decryption interceptor, to encrypt the request body, to decrypt the response message, obviously, this interceptor needs to be added to the network interceptor.

OkHttp’s Response object encapsulates the real Response message (networkResponse and cacheResponse). Therefore, as long as the response.body() method is not called in the interceptor, it will not block the request, especially if the response message is large.

Finally, there are three conclusions for CAHCE-Control:

  • MDN is an excellent site to understand Http protocol conventions correctly
  • Encounter problems read the source code, only the source code will not deceive
  • Practice is the sole criterion for testing truth