RetryAndFollowUpInterceptor is OKHttp first framework provides the interceptor, the variable name, mainly responsible for retry and redirect.

Why is the first interceptor responsible for this? What’s the point?

We think the interceptor overall operating mode, similar to the recursive one chain, from execution down on each link the result after the interceptor will result in the opposite direction on the chain transfer, then passing every interceptor have rights to deal with the result of the execution, can decorate the results, can abandon this result, also can re-execute the back of the link. RetryAndFollowUpInterceptor as the last interceptor, the execution results will return to his success in the interceptor. In a sort of bottom-of-the-pocket strategy, unsuccessful requests are returned to the interceptor for processing.

Graph TD Interceptor 1 --> Interceptor 2 interceptor 2 --> Interceptor 1 interceptor 2 --> Interceptor 3 interceptor 3 --> Interceptor 4 interceptor 4 --> Interceptor 3

Use location

Where was the first interceptor? See the following interceptor chain configuration code, is the external set of OKHttpClient interceptors, addInterceptor configuration. Indicates that the interceptor is the bottom interceptor. After the entire network request is completed, we can choose to perform operations, such as re-request the network, or configure the network data returned by the request, which can be said to be very free. Very large permissions. Even more powerful than this is our own interceptor. We can configure this variable to complete our own configuration. Flexibility is high.

List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); / / their own definition of interceptor interceptors. Add (new RetryAndFollowUpInterceptor (client)); // Interceptors. add(new BridgeInterceptor(client.cookiejar ())); .Copy the code

About RetryAndFollowUpInterceptor we see him first created and used to analyze his function: retry and redirection.

Create and use

RetryAndFollowUpInterceptor created in RealCall created when created, rather than the other interceptor was created when creating the interceptor chain. Why do you want to create it ahead of time.

  1. RetryAndFollowUpInterceptor provides a cancel cancel method, called RealCall will cancel to RetryAndFollowUpInterceptor that executes the cancel.
  2. Provides a RetryAndFollowUpInterceptorcaptureCallStackTraceMethods, this method can set up the stack information, because RetryAndFollowUpInterceptor is the first interceptor, equivalent to vanguard, initial information on the chain to be set by him.

The above two aspects reasons RetryAndFollowUpInterceptor need created ahead of time. The use aspect is relatively simple, directly inserted into the interception chain, waiting for the interception chain to run.

run

The interceptor specifically runs the interceptor code in the intercept(Chain Chain) method, which is called in via the RealInterceptorChain#process() method. The intercept(Chain Chain) method passes in the next RealInterceptorChain link, and internally calls process() to continue running the next link to intercept the Chain. Back and forth.

RetryAndFollowUpInterceptor @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Call call = realChain.call(); EventListener eventListener = realChain.eventListener(); StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; int followUpCount = 0; Response priorResponse = null; while (true) { if (canceled) { streamAllocation.release(); throw new IOException("Canceled"); } Response response; boolean releaseConnection = true; Response = realchain.proceed (request, streamAllocation, null, null); releaseConnection = false; } catch (RouteException e) { Decide whether you can retry} finally {if releaseConnection () {streamAllocation. StreamFailed (null); streamAllocation.release(); } } Request followUp; Try {// Get redirection request followUp = followUpRequest(response, streamallocation.route ()); } catch (IOException e) { streamAllocation.release(); throw e; } if (followUp == null) {streamallocation.release (); return response; } // Configure redirection requests... }}Copy the code

RetryAndFollowUpInterceptor intercept (Chain Chain) method of logic clearer, first create a StreamAllocation, this class is very important, later we will tell the class alone. Requests are processed and retry redirects are processed inside an infinite while loop, because the number of retries and redirects can be numerous. Proceed (Request, streamAllocation, NULL, NULL) with the next chain passed in to run the logic of the following interceptor chain. This is a synchronous operation that waits directly for the chain to complete and gets the final response. The retry logic is handled in exception catching. Redirection logic mainly obtains redirection new request Resquest through the followUpRequest method. Let’s talk about the next two parts.

Retry try again

The logic for retry is mainly in exception catching. RouteException and IOException, and use the Recover method to check whether it can be retried. If it can be retried, execute the continue because it is an infinite loop, and execute the next loop. Reexecuting realchain.proceed, that is, rerequesting the network, is the primary logic for retry.

try { response = realChain.proceed(request, streamAllocation, null, null); releaseConnection = false; } catch (RouteException e) {// The request may not have been issued if (! recover(e.getLastConnectException(), streamAllocation, false, request)) { throw e.getFirstConnectException(); } releaseConnection = false; continue; } catch (IOException e) {Boolean requestSendStarted =! (e instanceof ConnectionShutdownException); if (! recover(e, streamAllocation, requestSendStarted, request)) throw e; releaseConnection = false; continue; }Copy the code

In the RECOVER, RouteException, and IOException exceptions, the different arguments are the third argument, indicating whether the request has started. If RouteException is thrown, the request has not started. If an IOException, and are not ConnectionShutdownException type, said the request has already begun. Let’s look at the recover method. Returns true if it can be recovered and false if it cannot.

private boolean recover(IOException e, StreamAllocation streamAllocation, boolean requestSendStarted, Request userRequest) { streamAllocation.streamFailed(e); if (! client.retryOnConnectionFailure()) return false; if (requestSendStarted && requestIsUnrepeatable(e, userRequest)) return false; if (! isRecoverable(e, requestSendStarted)) return false; // No more routes can be tried. if (! streamAllocation.hasMoreRoutes()) return false; // return true; }Copy the code
  1. If set in OkHttpClientretryOnConnectionFailureThe field is false, indicating no retries on errors. thisThe first paper to mentionTo practically.
  2. If a request has already been made, but the request cannot be repeated, it cannot be retried. Equestbody of UnrepeatableRequestBody or exception of FileNotFoundException in requestIsUnrepeatable.
    private boolean requestIsUnrepeatable(IOException e, Request userRequest) {
          return userRequest.body() instanceof UnrepeatableRequestBody
          || e instanceof FileNotFoundException;
    }
    Copy the code
  3. Determine whether recovery isRecoverable by using isRecoverable. Returning true indicates that you can retry
    Private Boolean isRecoverable(IOException E, Boolean requestSendStarted) {// Do not restore if there is a protocol problem. if (e instanceof ProtocolException) { return false; } // If an interrupt occurs, you can retry the socket read/accept time timeout only if the request has not started and a SocketTimeoutException occurs. if (e instanceof InterruptedIOException) { return e instanceof SocketTimeoutException && ! requestSendStarted; } // If the client and server cannot negotiate the required level of security and the error is due to a certificate problem, If (e instanceof SSLHandshakeException) {if (LLDB instanceof CertificateException) {return false; }} // Indicates that the peer has not been authenticated. Can't try again if (e instanceof SSLPeerUnverifiedException) {/ / anyone with a certificate pinning the error. Return false. } return true; }Copy the code
  4. If the current routing available, no more streamAllocation. HasMoreRoutes () returns false. It cannot be retried.

The above is the specific logic of retry. If the recover method is used to determine whether the request can be retried, continue to execute the following logic, if not, directly throw an exception to end the request.

FollowUp redirect

Request followUp; try { followUp = followUpRequest(response, streamAllocation.route()); } catch (IOException e) { streamAllocation.release(); throw e; } if (followUp == null) { streamAllocation.release(); return response; } closeQuietly(response.body()); if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } if (followUp.body() instanceof UnrepeatableRequestBody) { streamAllocation.release(); throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); } if (! sameConnection(response, followUp.url())) { streamAllocation.release(); streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; } else if (streamAllocation.codec() ! = null) { throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?" ); } request = followUp; priorResponse = response;Copy the code

FollowUp redirects requests with FollowUp = followUpRequest(response, streamallocation.route ()). Successful requests returning 202 follow this logic. If a redirect request is available, the server needs us to redirect it. If no qualified request is received, the correct response is returned. Once a redirect request is obtained, the request is processed. The whole logic is these three parts. Let’s go through them one by one.

Gets the redirect request

With the followUpRequest method, the logic basically matches the redirected Http error code for processing. The redirection error code starts with a 3.

private Request followUpRequest(Response userResponse, Route route) throws IOException { if (userResponse == null) throw new IllegalStateException(); int responseCode = userResponse.code(); final String method = userResponse.request().method(); Switch (responseCode) {case HTTP_PROXY_AUTH:... Case HTTP_UNAUTHORIZED:... Case HTTP_PERM_REDIRECT: Case HTTP_TEMP_REDIRECT:... Case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER:... default: return null; }}Copy the code

The above code is quite long, so you can see that it handles not only the redirection status code starting with 3, but also 407, 401, etc. Let’s go through them one by one.

  1. HTTP_PROXY_AUTH 407 indicates that the proxy server requires our authentication

    Proxy selectedProxy = route.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);Copy the code

    As described above, if the proxy type is not Http, then some other type of proxy, such as SOCKS, or no proxy, is used and an exception is thrown. If it is currently valid, it is authenticated by proxyAuthenticator().authenticate. ProxyAuthenticator If we do not set it, it will be empty. If authentication is needed, implement Authenticator and rewrite Authenticate. Perform authentication.

  2. HTTP_UNAUTHORIZED 401 Indicates that the server requires authentication. Authenticator () is used directly to authenticate the server. Authenticator is an empty implementation by default. You need to implement Authenticator and override Authenticate. Same as above.

    client.authenticator().authenticate(route, userResponse);
    Copy the code
  3. HTTP_PERM_REDIRECT 308 HTTP_TEMP_REDIRECT 307 HTTP_MULT_CHOICE 306 HTTP_MOVED_PERM 305 HTTP_MOVED_TEMP 304 HTTP_SEE_OTHER 303 These are the redirected error codes. First 307 and 308 are a bit special. If a 307 or 308 status code is received in response and the request is made in a way other than GET or HEAD, the user agent cannot automatically redirect the request.

    case HTTP_PERM_REDIRECT: case HTTP_TEMP_REDIRECT: if (! method.equals("GET") && ! method.equals("HEAD")) { return null; }Copy the code

    The logic of redirecting the request comes later

  4. HTTP_CLIENT_TIMEOUT 408 408 is rare in practice, but some servers (such as HAProxy) use this response code. The specification says we can repeat the request without modification. Modern browsers also repeat requests (even if they are non-idempotent).

  5. HTTP_UNAVAILABLE 503 503 is an HTTP protocol server-side error status code that indicates that the server is not yet in a state to accept requests.

    if (userResponse.priorResponse() ! = null && userResponse. PriorResponse () code () = = HTTP_UNAVAILABLE) {/ / if the last response is also the same error code, then give up, not request return null; } if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) { // specifically received an instruction to retry without delay return userResponse.request();  } return null;Copy the code

    First, if the response code returned last time is also 503, then the request is not repeated. The retry-After field of header is determined using the retryAfter method. This field indicates the interval between the next request requested by the server. If the value is 0, the last request will be returned and the request will be retried.

    private int retryAfter(Response userResponse, int defaultDelay) {
      String header = userResponse.header("Retry-After");
    
          if (header == null) {
        return defaultDelay;
      }
    
      if (header.matches("\d+")) {
        return Integer.valueOf(header);
      }
    
      return Integer.MAX_VALUE;
    }
    Copy the code

The following is the specific processing of the redirection error code starting with 3.

if (! client.followRedirects()) return null; String location = userResponse.header("Location"); if (location == null) return null; HttpUrl url = userResponse.request().url().resolve(location); if (url == null) return null; boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme()); if (! sameScheme && ! client.followSslRedirects()) return null; 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"); } } if (! sameConnection(userResponse, url)) { requestBuilder.removeHeader("Authorization"); } return requestBuilder.url(url).build();Copy the code

The first few lines of the block are intercepted. These cases cannot be redirected. After passing the block, the redirected request is constructed.

Create interception phase
  1. Redirect cannot be configured in OKHttpClient if client.followredirects () is falsefollowRedirectsFalse: redirection is not allowed. The default value is true.
  2. A redirected response will have a Location that represents the destination of the redirect. If there is no Location or if the Location is empty inside, it will be illegal and cannot be redirected.
  3. If the current redirected address and the original requested addressschemeDifferent, which means one is HTTP and one is HTTPS. The redirection is cross-protocol. In OKHttpClient, there is also a parameter configured to indicate whether this redirection is possible, namelyfollowSSLRedirects, the default is true. If we set it to false, this redirection cannot take place.
Real build phase

RequestBuilder requestBuilder = userResponse.request().newBuilder() copies a new Request. Then reconfigure the request based on the requested method.

  1. If the current Request is not Get, we need to reset the body of the Request
  2. In addition toPROPFINDAll other requests are redirected to Get requests.PROPFINDA request of type will carry the body of the response returned.
  3. PROPFINDFor other types, clear the transfer-Encoding, Content-Length, and Content-Type tags
  4. If sameConnection returns false, that is, if the request is cross-host, the authentication flag in the header needs to be cleared.

This is the implementation of the redirection logic in OkHttp.

The next article covers BridgeInterceptor, the second interceptor provided by the framework