instructions

OKHTTP internal has many interceptor respectively responsible for different functions, including internal RetryAndFollowUpInterceptor OKHTTP retry mechanism is mainly responsible for the request, today we’ll look at how OKHTTP implement retry mechanism;

First, let’s look at how to turn on or off the default retry mechanism.

OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.retryOnConnectionFailure(true);
Copy the code

The retry mechanism is enabled by default.

The source code parsing

RetryAndFollowUpInterceptor retry mechanism is the key code is as follows:

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      // 1. First try to create a request flow and prepare the link
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
       //2. Start execution, enter the follow-up interceptor, the actual network request;
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // 3. RouteException occurs: the request is not sent and the first link exception is thrown
        if(! recover(e.getLastConnectException(), transmitter,false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        // 4. If IOException occurs, check whether the RouteException logic can be restored. See the preceding RouteException logic
        booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
        if(! recover(e, transmitter, requestSendStarted, request))throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      //5. Build a new Response object based on the previous Response result, and the body of this object is empty
      if(priorResponse ! =null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      //6. Create a new request based on the request code for the next retry requestExchange exchange = Internal.instance.exchange(response); Route route = exchange ! =null ? exchange.connection().route() : null;
      Request followUp = followUpRequest(response, route);

     //7. If the Request constructed in step 6 is empty, the Response is returned
      if (followUp == null) {
        if(exchange ! =null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      //8. The constructed Request object contains the Request body and is a one-time Request.
      RequestBody followUpBody = followUp.body();
      if(followUpBody ! =null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }
      // 9. Check whether the maximum number of retries has been reached (20 by default). If so, an exception is thrown
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //10. If the above does not throw an exception or break the loop, the while loop is entered and the next retry process beginsrequest = followUp; priorResponse = response; }}Copy the code

1. First try to create a request flow and prepare the link;

2. Start execution, enter the follow-up interceptor, and actually make network request;

3. RouteException: Indicates that the route link is abnormal and does not work properly after multiple retries. Then determine whether the current route exception can be recovered. If it cannot be recovered, FirstConnectException will be thrown. If it can be recovered, it will be enriched.

/** * Whether to restore */
private boolean recover(IOException e, Transmitter transmitter,
      boolean requestSendStarted, Request userRequest) {
    // The application layer has forbidden retries.
    if(! client.retryOnConnectionFailure())return false;

    // We can't send the request body again.
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;

    // This exception is fatal.
    if(! isRecoverable(e, requestSendStarted))return false;

    // No more routes to attempt.
    if(! transmitter.canRetry())return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

  private boolean requestIsOneShot(IOException e, Request userRequest) {
    RequestBody requestBody = userRequest.body();
    return(requestBody ! =null && requestBody.isOneShot())
        || e instanceof FileNotFoundException;
  }

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // If there was a protocol problem, don't recover.
    if (e instanceof ProtocolException) {
      return false;
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    if (e instanceof InterruptedIOException) {
      return e instanceofSocketTimeoutException && ! requestSendStarted; }// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false; }}if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false;
    }

    // An example of one we might want to retry with a different route is a problem connecting to a
    // proxy and would manifest as a standard IOException. Unless it is one we know we should not
    // retry, we return true and try a new route.
    return true;
  }
Copy the code

The code logic is as follows:

1.First, check whether retry is allowed. According to the retry switch configured when the request is created, retry is not allowed.2.The second layer determines that if the request has been started and the current request can be sent only once at most, a retry is not allowed.3.To determine whether the current request is recoverable, the following abnormal scenarios are unrecoverable: A. ProtocolException B. SocketTimeoutException C. SSLHandshakeException && CertificateException: Indicates that the security level of the Socket does not match that of the server. This connection is unavailable. D. SSLPeerUnverifiedException peer entity authentication exception, that is to say individual peer is not authenticated, similar without certificate, or during the handshake did not establish peer-to-peer individual validation;4.Check whether another route exists that can be retried. If no route exists, retry is not allowed.5.If no, try again.Copy the code

4. If IOException occurs, whether the judgment logic can be restored refer to the above RouteException judgment logic.

5. Build a new Response object according to the last Response result, and the body of this object is empty;

6. A new request is created based on the request code for the next request to be retried. The authentication header information is added based on the result of the previous request, redirection is traced, or the client request times out.

The code is as follows:

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: Proxy selectedProxy = route ! =null
            ? route.proxy()
            : client.proxy();
        if(selectedProxy.type() ! = Proxy.Type.HTTP) {throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);

      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if(! method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if(! client.followRedirects())return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if(! sameScheme && ! client.followSslRedirects())return null;

        // Most redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (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"); }}// When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if(! sameConnection(userResponse.request().url(), url)) { requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if(! client.retryOnConnectionFailure()) {// The application layer has directed us not to retry the request.
          return null;
        }

        RequestBody requestBody = userResponse.request().body();
        if(requestBody ! =null && requestBody.isOneShot()) {
          return null;
        }

        if(userResponse.priorResponse() ! =null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();

      case HTTP_UNAVAILABLE:
        if(userResponse.priorResponse() ! =null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        return null; }}Copy the code

A different Request object is returned based on the result code (returning null to indicate that the current Request trace is unnecessary or applicable) :

  1. HTTP_PROXY_AUTH (407) : proxy authentication, required proxy authentication (default implementation returns null, can be overridden implementation);

  2. HTTP_UNAUTHORIZED (401) : Unauthorized and requires authorization (the default implementation returns NULL, which can be overwritten)

  3. HTTP_PERM_REDIRECT (307) or HTTP_TEMP_REDIRECT (308) : temporary or permanent redirection. If the request METHOD is not GET or METHOD, no value is returned, indicating that redirection is not allowed.

  4. HTTP_MULT_CHOICE (300) multi-option HTTP_MOVED_PERM (301) permanent redirection, indicating that the requested resource has been assigned a new URI, should use the new URI HTTP_MOVED_TEMP (302) temporary redirection, HTTP_MOVED_TEMP (303) indicates that because the requested resource has another URI, the GET method should be used to target the requested resource

    The logic corresponding to these four codes is as follows:

    1. First, determine whether redirection is allowed and return null is not allowed.
    2. And then I’m going to tell you if the locations head is going to parse, and if it’s not going to parse, it’s going to return empty;
    3. The scheme returned is inconsistent with the requested scheme and does not allow SSL redirection.
    4. If the request body exists, build the GET request if it can be redirected to GET, otherwise build the original request;
    5. Delete all authentication headers when redirecting across hosts.
    6. Finally, the Request Request object is constructed.
  5. HTTP_CLIENT_TIMEOUT (408) : The request times out. The logic is as follows

    1. If retries are not allowed, null is returned
    2. If the current Response is not empty and only one request is allowed, then null is returned
    3. If both the current request result and the previous request result time out, retry is abandoned
    4. The parse results in a retry-after header (the HTTP header of the response indicates how long the user agent should wait before making a subsequent request) : null if the current retry-after is greater than 0;
  6. HTTP_UNAVAILABLE (503) : Indicates that the server is temporarily overloaded or is down for maintenance and cannot process requests.

    1. If 503 is returned in both the result of this request and the result of the last request, retry is abandoned
    2. If retry-after is 0 with no delay, the Request object is returned, otherwise null;

7. If the Request constructed in step 6 is empty, the process is stopped and Response is returned directly

8. If the constructed Request object contains the Request body and is a one-time Request, the Response is directly returned without a retry.

9. Check whether the maximum number of retries has been reached (20 by default). If so, an exception is thrown.

10. If the above does not throw an exception or break the loop, the while loop is entered and the next retry process begins.

To this end RetryAndFollowUpInterceptor retry mechanism analysis.