Hello, I’m N0tExpectErr0r, an Android developer who loves technology

My personal blog: blog.n0tExpecterr0r.cn

OkHttp source code analysis series

OkHttp source code Analysis series (1) – request initiation and interceptor mechanism overview

OkHttp source code analysis series (two) – interceptor overall flow analysis

OkHttp source code analysis series (three) – caching mechanism

OkHttp source code analysis series (4) – connection establishment overview

OkHttp source code analysis series (five) – proxy routing

OkHttp source code analysis series (six) – connection reuse mechanism and connection establishment

OkHttp source code analysis series (seven) – request initiation and response read

Previous articles provided an overview of OkHttp’s interceptor mechanism, so let’s take a look at its interceptor implementation in turn.

RetryAndFollowUpInterceptor

Mentioned earlier, RetryAndFollowUpInerceptor is responsible for the redirection function of HTTP requests, that let us know about the redirection of the HTTP protocol.

Redirection in HTTP

The HTTP protocol provides a redirection function that triggers client redirection by sending back a response in a specific format from the server. The corresponding Response Code format is 3XX, and a new URL will be put into the Location field of Response Header, so that our client can request again according to the URL specified in the Location field to get the required data.

The process is shown in the figure below:

The redirection status codes and their meanings are shown in the following table (from Wikipedia) :

Difference between redirection and server forwarding

As you can see, redirects are somewhat similar to server forwarding requests. What’s the difference?

  1. Redirection is a client action, while server forwarding is a server action

  2. The client that redirected us made multiple requests, while the client that forwarded us made only one.

  3. The control of redirection resides on the client and the control of forwarding resides on the server.

### Code analysis

Let’s study the RetryAndFollowUpInterceptor the principle, we see RetryAndFollowUpInterceptor. Intercept method:

@Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Transmitter = realchain.transmitter (); int followUpCount = 0; Response priorResponse = null;while (true) {/ / do some preparation work before connection transmitter. PrepareToConnect (request); // Handle cancel eventsif (transmitter.isCanceled()) {
            throw new IOException("Canceled");
        }
        Response response;
        boolean success = false; Response = realchain. proceed(request, transmitter, null); success =true; } catch (RouteException e) {// Throw an exception if the redirection condition is not metif(! recover(e.getLastConnectException(), transmitter,false, request)) { throw e.getFirstConnectException(); } // If the redirection conditions are met, try againcontinue; } catch (IOException e) { boolean requestSendStarted = ! (e instanceof ConnectionShutdownException); // An exception is thrown if the redirection condition is not metif(! recover(e, transmitter, requestSendStarted, request)) throw e; // If the redirection conditions are met, try againcontinue;
        } finally {
            if(! If success) {/ / throws exceptions, release resources transmitter. ExchangeDoneDueToException (); }} // Set the previous response in response, whose body is emptyif(priorResponse ! = null) { response = response.newBuilder() .priorResponse(priorResponse.newBuilder() .body(null) .build()) .build(); } Exchange exchange = Internal.instance.exchange(response); Route route = exchange ! = null ? exchange.connection().route() : null; Request followUp = followUpRequest(response, route);if(followUp == null) {// No redirection is required, stop timeout and return responseif(exchange ! = null && exchange.isDuplex()) { transmitter.timeoutEarlyExit(); }return response;
        }
        RequestBody followUpBody = followUp.body();
        if(followUpBody ! = null && followUpBody.isOneShot()) {return response;
        }
        closeQuietly(response.body());
        if(transmitter.hasExchange()) { exchange.detachWithViolence(); } // No more than 20 redirects, otherwise an exception is thrownif (++followUpCount > MAX_FOLLOW_UPS) {
            throw new ProtocolException("Too many follow-up requests: "+ followUpCount); } // Modify the following redirection request request = followUp; PriorResponse = response; }}Copy the code

As you can see, there is a loop around the outside that is constantly redirecting, so let’s see what’s going on inside the loop:

  1. Do some pre-processing
  2. callchain.proceedMethod to request the fetchResponse
  3. If the underlying layer throws an exception during the process, a redirect is attempted
  4. If the redirection conditions are not met, an exception is thrown
  5. If other unknown exceptions occur, resources are released by throwing exceptions
  6. In thisResponseTo set the previous ResponsepriorResponseAnd body is empty
  7. According to theResponseThe response code is redirected and calledfollowUpRequestMethod to get the redirected requestfollowUp
  8. If redirectedfollowUpIf the value is null, the redirection is no longer required. Stop timeout and returnResponse
  9. If the redirection exceeds the specified number of times (20 by default), an exception is thrown.
  10. If it still does not return, the next redirect is required, to the nextrequestAnd so on.

Let’s see what the followUpRequest method does:

private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();
    final String method = userResponse.request().method();
    switch (responseCode) {
        caseHTTP_PROXY_AUTH: // 407 // ... // Proxy authenticationcaseHTTP_UNAUTHORIZED: // 401 // ... // Identity authenticationcase HTTP_PERM_REDIRECT:	// 308
        caseHTTP_TEMP_REDIRECT: // 307 // 307/308 Do not redirect requests other than GET and HEADif(! method.equals("GET") && !method.equals("HEAD")) {
                return null;
            }
        case HTTP_MULT_CHOICE:		// 300
        case HTTP_MOVED_PERM:		// 301
        case HTTP_MOVED_TEMP:		// 302
        caseHTTP_SEE_OTHER: // 303 // Null is returned if the client has turned off redirectionif(! client.followRedirects())returnnull; String location = userResponse.header(String location = userResponse.header)"Location");
            if (location == null) returnnull; HttpUrl url = userResponse.request().url().resolve(location); / /... Request.Builder requestBuilder = userResponse.request().newBuilder(); // The method used to handle redirectsif (HttpMethod.permitsRequestBody(method)) {
                final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                if (HttpMethod.redirectsToGet(method)) {
                    requestBuilder.method("GET", null);
                } else {
                    RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                    requestBuilder.method(method, requestBody);
                }
                if(! maintainBody) { requestBuilder.removeHeader("Transfer-Encoding");
                    requestBuilder.removeHeader("Content-Length");
                    requestBuilder.removeHeader("Content-Type"); }} // Rebuild requestreturn requestBuilder.url(url).build();
        caseHTTP_CLIENT_TIMEOUT: // 408 // 408 Indicates the same request needs to be sent again //...return userResponse.request();
        case HTTP_UNAVAILABLE:		// 503
            // ...
            return null;
        default:
            returnnull; }}Copy the code

As can be seen, the redirected request is constructed by extracting Location field from several redirected status codes.

BridgeInterceptor

BridgeInterceptor is aptly named because it acts as a bridge between users and the server. When the user sends a Request to the server, it converts the user-constructed Request into a real Request to the server, and when the server returns a Response, it converts the Response from the server into a Response that the user can use.

Let’s see BridgeInterceptor. Intercept method:

@Override public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); RequestBody body = userRequest.body(); // Set some of the userRequest properties into the Builderif(body ! = null) { MediaType contentType = body.contentType();if(contentType ! = null) { requestBuilder.header("Content-Type", contentType.toString());
        }
        long contentLength = body.contentLength();
        if(contentLength ! = -1) { requestBuilder.header("Content-Length", Long.toString(contentLength));
            requestBuilder.removeHeader("Transfer-Encoding");
        } else {
            requestBuilder.header("Transfer-Encoding"."chunked");
            requestBuilder.removeHeader("Content-Length"); }}if (userRequest.header("Host") == null) {
        requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
    if (userRequest.header("Connection") == null) {
        requestBuilder.header("Connection"."Keep-Alive");
    }
    boolean transparentGzip = false; // If accept-encoding is not set, gzip is automatically setif (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
        transparentGzip = true;
        requestBuilder.header("Accept-Encoding"."gzip"); } // set cookies in userRequest to Builder List<Cookie> cookies = cookiejar.loadForRequest (userRequest.url());if(! cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } // Set user-agentif (userRequest.header("User-Agent") == null) {
        requestBuilder.header("User-Agent", Version.userAgent()); Responsenetworkresponse = chain.proceed(requestBuilder.build()); / / to deal with the response headers HttpHeaders receiveHeaders (cookieJar userRequest. Url (), networkResponse headers ()); // Build a new Response based on the server's Response, And sets the userRequest to its request Response. Builder responseBuilder. = networkResponse newBuilder (.) the request (userRequest); // If gzip compression has been set and response contains gzip compression, gzip decompression is performedif (transparentGzip
            && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source());  Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding")
                .removeAll("Content-Length")
                .build();
        responseBuilder.headers(strippedHeaders);
        String contentType = networkResponse.header("Content-Type");
        responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
}
Copy the code

As you can see, we’re processing the headers. We’re processing some of the headers from the original request and putting them into the new request. If the caller does not set accept-encoding, it defaults to gzip.

In response processing, if gzip is set before, gzip decompression is performed. This automatic decompression automatically removes the Content-Length and Content-Encoding fields from the Header, so the upper layer might get -1.

Cookie processing is not a concern for now and will be covered in a future article.

CacheInterceptor

CacheInterceptor is responsible for reading and updating the cache. Let’s take a look at its Intercept method:

@Override public Response Intercept (Chain Chain) throws IOException {// Try to obtain the cache Response cacheCandidate = cache! = null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); // Pass in the current time, request, and the cache fetched from the cache. CacheStrategy Strategy = new cacheStrategy.factory (now, chain-.request (), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response Response cacheResponse = strategy.cacheresponse;if(cache ! = null) { cache.trackResponse(strategy); }if(cacheCandidate ! = null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // If The network is not available and there is no cache, The request fails. Construct a Response that fails and return itif (networkRequest == null && cacheResponse == null) {
        return new Response.Builder()
                .request(chain.request())
                .protocol(Protocol.HTTP_1_1)
                .code(504)
                .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); } // If no network request is required, return directlyif (networkRequest == null) {
        returncacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } Response networkResponse = null; Try {// networkResponse = chain.proceed(networkRequest); } finally {// If a crash occurs during I/O, recycle the resourceif(networkResponse == null && cacheCandidate ! = null) { closeQuietly(cacheCandidate.body()); }} // If there is a cache in the cache and the requested code is 304, the result of the cache and network request will be returned, and the contents of the cache will be updatedif(cacheResponse ! = null) {if (networkResponse.code() == HTTP_NOT_MODIFIED) {	// 304
            Response response = cacheResponse.newBuilder()
                    .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                    .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                    .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                    .cacheResponse(stripBody(cacheResponse))
                    .networkResponse(stripBody(networkResponse))
                    .build();
            networkResponse.body().close();
            // Update the cache after combining headers but before stripping the
            // Content-Encoding header (as performed by initContentStream()).
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            return response;
        } else{ closeQuietly(cacheResponse.body()); }} / / build the response the response response. = networkResponse newBuilder () cacheResponse (stripBody (cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); // Cache the request responseif(cache ! = null) {if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // Offer this request to the cache.
            CacheRequest cacheRequest = cache.put(response);
            return cacheWritingResponse(cacheRequest, response);
        }
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
                cache.remove(networkRequest);
            } catch (IOException ignored) {
                // The cache cannot be written.
            }
        }
    }
    return response;
}
Copy the code

As you can see, the main steps are as follows

  1. An attempt was made to fetch a cached object from the cacheresponse
  2. According to the current time,requestAnd the cacheresponseBuild a cache policy.
  3. If the cache policy cannot use the network (networkRequest == null), and no cache (cacheResponse == null), the direct request fails.
  4. If the cache policy cannot use the network, it can be determined that there is a cache because of the previous judgment, and directly build the cacheresponseAnd return.
  5. callchain.proceedNetwork Request Acquisitionresponse
  6. Code 304 was processed and constructed with local and network returned dataresponseAnd return
  7. What is obtained by building network requestsresponseAnd because the network request has not been cached, it is cached and the result is returned

As for the specific implementation of caching, here is no more than a brief introduction. Later, we will open a special article for analysis, mainly focusing on the process.

ConnectInterceptor

The ConnectInterceptor is responsible for establishing a connection to the server. Its code is very short:

@Override
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    Transmitter transmitter = realChain.transmitter();
    boolean doExtensiveHealthChecks = ! request.method().equals("GET"); Exchange = transmitter. NewExchange (chain,doExtensiveHealthChecks);
    return realChain.proceed(request, transmitter, exchange);
}
Copy the code

NewExchange is called to create an Exchange, and the method realchain.proceed (request, transmitter, Exchange) is called.

What exactly is this Exchange class? We see its JavaDoc:

Transmits a single HTTP request and a response pair. This layers connection management and events on {@link ExchangeCodec}, which handles the actual I/O.

That is, the Exchange class can layer the connection management and events of the Exchange Ecodec class, which is a class that actually does I/O, so it looks like the class is doing some connection management transactions. A connection between a client and a server may be created/reused during the course of newExchange.

We will not introduce the specific connection acquisition process here for the moment, and it will be introduced in detail in subsequent articles. This article is more inclined to explain the overall process.

CallServerInterceptor

The CallServerInterceptor is the last interceptor in the network request chain. It actually reads the Response from the server. Let’s take a look at its implementation:

@Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Exchange exchange = realChain.exchange(); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); / / write request head exchange. WriteRequestHeaders (request). boolean responseHeadersStarted =false;
    Response.Builder responseBuilder = null;
    if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! = null) {// Do special processing for the 100-continue headerif ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            exchange.flushRequest();
            responseHeadersStarted = true;
            exchange.responseHeadersStart();
            responseBuilder = exchange.readResponseHeaders(true);
        }
        if(responseBuilder == null) {// Write the request bodyif (request.body().isDuplex()) {
                // Prepare a duplex body so that the application can send a request body later.
                exchange.flushRequest();
                BufferedSink bufferedRequestBody = Okio.buffer(
                        exchange.createRequestBody(request, true));
                request.body().writeTo(bufferedRequestBody);
            } else {
                // Write the request body if the "Expect: 100-continue" expectation was met.
                BufferedSink bufferedRequestBody = Okio.buffer(
                        exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); }}else {
            exchange.noRequestBody();
            if(! exchange.connection().isMultiplexed()) { // If the"Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection // from being reused. Otherwise we're still obligated to transmit the request body to
                // leave the connection ina consistent state. exchange.noNewExchangesOnConnection(); }}}else {
        exchange.noRequestBody();
    }
    if(request.body() == null || ! request.body().isDuplex()) { exchange.finishRequest(); }if(! responseHeadersStarted) { exchange.responseHeadersStart(); }if(responseBuilder = = null) {/ / read response headers responseBuilder = exchange. ReadResponseHeaders (false);
    }
    Response response = responseBuilder
            .request(request)
            .handshake(exchange.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    int code = response.code();
    if (code == 100) {
        // server sent a 100-continue even though we did not request one.
        // try again to readThe actual response / / read response headers response = exchange. ReadResponseHeaders (false) .request(request) .handshake(exchange.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); code = response.code(); } exchange.responseHeadersEnd(response); // Read the response bodyif (forWebSocket && code == 101) {
        // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
        response = response.newBuilder()
                .body(Util.EMPTY_RESPONSE)
                .build();
    } else {
        response = response.newBuilder()
                .body(exchange.openResponseBody(response))
                .build();
    }
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
        exchange.noNewExchangesOnConnection();
    }
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
        throw new ProtocolException(
                "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
}
Copy the code

There is a lot of code here, but the core is the following steps:

  1. writeRequest Header
  2. writeRequest Body
  3. readResponse Header
  4. readResponse Body

The specific implementation will be introduced in subsequent articles, and the general process of the whole chain of responsibility will be analyzed here.

The resources

The HTTP status code

OkHttp Tour series