OKHttp攔截器-重試和重定向攔截器

經(jīng)過(guò)上一篇的解析,我們已經(jīng)對(duì)OKHttp的同步請(qǐng)求和異步請(qǐng)求了然于胸,還有五大攔截器可以說(shuō)是它的畫龍點(diǎn)睛之筆,今天我們就來(lái)看看,它們是怎么運(yùn)作的。

RetryAndFollowUpInterceptor,顧名思義,用來(lái)處理請(qǐng)求失敗后重連和重定向的,上一篇我們知道了責(zé)任鏈調(diào)用的是intercept()方法:

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
  val realChain = chain as RealInterceptorChain
  var request = chain.request
  val call = realChain.call
  var followUpCount = 0
  var priorResponse: Response? = null
  var newExchangeFinder = true
  var recoveredFailures = listOf<IOException>()
  while (true) {
    call.enterNetworkInterceptorExchange(request, newExchangeFinder)

    var response: Response
    var closeActiveExchange = true
    try {
      if (call.isCanceled()) {
        throw IOException("Canceled")
      }

      try {
        // 
        response = realChain.proceed(request)
        newExchangeFinder = true
      } catch (e: RouteException) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
          throw e.firstConnectException.withSuppressed(recoveredFailures)
        } else {
          recoveredFailures += e.firstConnectException
        }
        newExchangeFinder = false
        continue
      } catch (e: IOException) {
        // An attempt to communicate with a server failed. The request may have been sent.
        if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
          throw e.withSuppressed(recoveredFailures)
        } else {
          recoveredFailures += e
        }
        newExchangeFinder = false
        continue
      }

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
            .build()
      }

      val exchange = call.interceptorScopedExchange
      val followUp = followUpRequest(response, exchange)

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex) {
          call.timeoutEarlyExit()
        }
        closeActiveExchange = false
        return response
      }

      val followUpBody = followUp.body
      if (followUpBody != null && followUpBody.isOneShot()) {
        closeActiveExchange = false
        return response
      }

      response.body?.closeQuietly()

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw ProtocolException("Too many follow-up requests: $followUpCount")
      }

      request = followUp
      priorResponse = response
    } finally {
      call.exitNetworkInterceptorExchange(closeActiveExchange)
    }
  }
}

攔截器代碼有點(diǎn)多,我們分步驟來(lái)看,首先是一個(gè)while死循環(huán),因?yàn)槲覀兂霈F(xiàn)異常后可能需要重試第二次、第三次...,所以這里用了一個(gè)死循環(huán),將請(qǐng)求進(jìn)行try catch捕獲,如果沒(méi)有異常,判斷是否需要重定向,如果不需要,直接返回response,否則重新創(chuàng)建一個(gè)Request進(jìn)行請(qǐng)求,并返回response;如果出現(xiàn)了異常,進(jìn)入catch模塊。

重試

RouteException

catch (e: RouteException) {
    if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
        throw e.firstConnectException.withSuppressed(recoveredFailures)
    } else {
        recoveredFailures += e.firstConnectException
    }
    newExchangeFinder = false
    continue
}

判斷recover(),如果返回false,直接拋出異常,否則直接continue進(jìn)入下一次循環(huán),循環(huán)后還是走的try語(yǔ)句塊,這樣就實(shí)現(xiàn)了重連機(jī)制,不用想,recover()肯定就是判斷是否可以重試了。

private fun recover(
  e: IOException,
  call: RealCall,
  userRequest: Request,
  requestSendStarted: Boolean
): Boolean {
  // 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 (!call.retryAfterFailure()) return false

  // For failure recovery, use the same route selector with a new connection.
  return true
}
  1. !client.retryOnConnectionFailure為false, 那么不允許重試, 這個(gè)是我們創(chuàng)建OKHttpClient的時(shí)候進(jìn)行的配置,默認(rèn)為true,如果我們?cè)O(shè)置了false,就不會(huì)重試了

  2. if (requestSendStarted && requestIsOneShot(e, userRequest)) return false 
    
    private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
        val requestBody = userRequest.body
        return (requestBody != null && requestBody.isOneShot()) ||
            e is FileNotFoundException
      }
    

    如果requestBody.isOneShot()為true, 或者異常類型為文件未找到,就不會(huì)進(jìn)行重試了,如果請(qǐng)求為post請(qǐng)求時(shí),需要我們傳遞一個(gè)RequestBody對(duì)象,它是一個(gè)抽象類,isOneShot()默認(rèn)返回false,如果我們需要某一個(gè)接口特殊處理,就可以重寫此方法:

    class MyRequestBody : RequestBody() {
        override fun contentType(): MediaType? {
            return null
        }
    
        override fun writeTo(sink: BufferedSink) {
        }
     // 覆蓋此方法,返回true,代表不要進(jìn)行重試
        override fun isOneShot(): Boolean {
            return true
        }
    }
    
  3. if (!isRecoverable(e, requestSendStarted)) return false,這個(gè)方法判斷一些異常類型,某些異常時(shí)不可以重試:

    private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
      // If there was a protocol problem, don't recover.
      if (e is 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 is InterruptedIOException) {
        return e is SocketTimeoutException && !requestSendStarted
      }
    
      // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
      // again with a different route.
      if (e is SSLHandshakeException) {
        // If the problem was a CertificateException from the X509TrustManager,
        // do not retry.
        if (e.cause is CertificateException) {
          return false
        }
      }
      if (e is 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
    }
    
    • ProtocolException(協(xié)議異常)時(shí),不允許重試;
    • InterruptedIOException(IO中斷異常),如果是因?yàn)檫B接超時(shí)那么就允許重試,反之不可以;
    • SSLHandshakeException(SSL握手異常時(shí)),鑒權(quán)失敗了就不可以重試;
    • SSLPeerUnverifiedException(證書過(guò)期 or 失效),不可以重試;
  4. if (!call.retryAfterFailure()) return false,判斷有沒(méi)有可以用來(lái)連接的路由路線,如果沒(méi)有就返回false,如果存在更多的線路,那么就會(huì)嘗試換條線路進(jìn)行重試。

IOException

catch (e: IOException) {
  // An attempt to communicate with a server failed. The request may have been sent.
  if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
    throw e.withSuppressed(recoveredFailures)
  } else {
    recoveredFailures += e
  }
  newExchangeFinder = false
  continue
}

同樣是調(diào)用recover()方法進(jìn)行判斷,這里就不多講了。

重定向

如果請(qǐng)求的過(guò)程中沒(méi)有拋出異常,那么就要判斷是否可以重定向。

val followUp = followUpRequest(response, exchange)

if (followUp == null) {
  if (exchange != null && exchange.isDuplex) {
    call.timeoutEarlyExit()
  }
  closeActiveExchange = false
  return response
}

val followUpBody = followUp.body
if (followUpBody != null && followUpBody.isOneShot()) {
  closeActiveExchange = false
  return response
}

response.body?.closeQuietly()

if (++followUpCount > MAX_FOLLOW_UPS) {
  throw ProtocolException("Too many follow-up requests: $followUpCount")
}

調(diào)用followUpRequest()方法獲取重定向之后的Request。

如果不允許重定向,就返回null,這時(shí)候直接把response返回即可;

如果允許重定向,獲取新的請(qǐng)求體,判斷followUpBody.isOneShot()為true,代表不可以重定向,直接返回response;

否則使用新的Request進(jìn)行請(qǐng)求。

@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
  val route = exchange?.connection?.route()
  val responseCode = userResponse.code

  val method = userResponse.request.method
  when (responseCode) {
    HTTP_PROXY_AUTH -> {
      val selectedProxy = route!!.proxy
      if (selectedProxy.type() != Proxy.Type.HTTP) {
        throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
      }
      return client.proxyAuthenticator.authenticate(route, userResponse)
    }

    HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)

    HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
      return buildRedirectRequest(userResponse, method)
    }

    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
      }

      val requestBody = userResponse.request.body
      if (requestBody != null && requestBody.isOneShot()) {
        return null
      }
      val priorResponse = userResponse.priorResponse
      if (priorResponse != null && 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
    }

    HTTP_UNAVAILABLE -> {
      val priorResponse = userResponse.priorResponse
      if (priorResponse != null && 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
    }

    HTTP_MISDIRECTED_REQUEST -> {
      // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
      // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
      // we can retry on a different connection.
      val requestBody = userResponse.request.body
      if (requestBody != null && requestBody.isOneShot()) {
        return null
      }

      if (exchange == null || !exchange.isCoalescedConnection) {
        return null
      }

      exchange.connection.noCoalescedConnections()
      return userResponse.request
    }

    else -> return null
  }
}

根據(jù)服務(wù)器響應(yīng)的code判斷是否進(jìn)行重定向

  • HTTP_PROXY_AUTH:407 客戶端使用了HTTP代理服務(wù)器,如果在請(qǐng)求頭中添加了Proxy-Authorization,讓代理服務(wù)器授權(quán)進(jìn)行重定向

  • HTTP_UNAUTHORIZED:401 需要身份驗(yàn)證,有些服務(wù)器接口需要驗(yàn)證使用者身份 在請(qǐng)求頭中添加Authorization

  • **HTTP_PERM_REDIRECT(308), **永久重定向

    **HTTP_TEMP_REDIRECT(307), **臨時(shí)重定向

    HTTP_MULT_CHOICE(300),

    **HTTP_MOVED_PERM(301), **

    HTTP_MOVED_TEMP(302),

    HTTP_SEE_OTHER(303)

    private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
       
        if (!client.followRedirects) return null
      // 1. 如果請(qǐng)求頭中沒(méi)有Location , 那么沒(méi)辦法重定向
        val location = userResponse.header("Location") ?: return null
        // 2. 解析Location請(qǐng)求頭中的url,如果不是正確的url,返回null
        val url = userResponse.request.url.resolve(location) ?: return null
    
        // 3. 如果重定向在http到https之間切換,需要檢查用戶是不是允許(默認(rèn)允許)
        val sameScheme = url.scheme == userResponse.request.url.scheme
        if (!sameScheme && !client.followSslRedirects) return null
     
        val requestBuilder = userResponse.request.newBuilder()
        // 4.判斷請(qǐng)求是不是get或head
        if (HttpMethod.permitsRequestBody(method)) {
          val responseCode = userResponse.code
          val maintainBody = HttpMethod.redirectsWithBody(method) ||
              responseCode == HTTP_PERM_REDIRECT ||
              responseCode == HTTP_TEMP_REDIRECT
           // 5. 重定向請(qǐng)求中 只要不是 PROPFIND 請(qǐng)求,無(wú)論是POST還是其他的方法都要改為GET請(qǐng)求方式,即只有 PROPFIND 請(qǐng)求才能有請(qǐng)求體
          // HttpMethod.redirectsToGet(method) 判斷是否是PROPFIND,不是返回true
          if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
            requestBuilder.method("GET", null)
          } else {
            // 如果是PROPFIND請(qǐng)求,添加請(qǐng)求體
            val requestBody = if (maintainBody) userResponse.request.body else null
            requestBuilder.method(method, requestBody)
          }
          // 6. 不是 PROPFIND 的請(qǐng)求,把請(qǐng)求頭中關(guān)于請(qǐng)求體的數(shù)據(jù)刪掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding")
            requestBuilder.removeHeader("Content-Length")
            requestBuilder.removeHeader("Content-Type")
          }
        }
        // 7. 在跨主機(jī)重定向時(shí),刪除身份驗(yàn)證請(qǐng)求頭
        if (!userResponse.request.url.canReuseConnectionFor(url)) {
          requestBuilder.removeHeader("Authorization")
        }
      // 返回Request對(duì)象
        return requestBuilder.url(url).build()
    }
    

    如果是以上幾種狀態(tài),會(huì)走的這里的代碼,并返回Request對(duì)象,其中每一步都有注釋,這里就不一一贅述了。

  • HTTP_CLIENT_TIMEOUT:408,客戶端請(qǐng)求超時(shí),算是請(qǐng)求失敗了,這里其實(shí)是走重試邏輯了

    • if (!client.retryOnConnectionFailure):先判斷用戶是否允許重試
    • if (requestBody != null && requestBody.isOneShot()):判斷本次請(qǐng)求是否可以重試
    • if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT):如果是本身這次的響應(yīng)就是重新請(qǐng)求的產(chǎn)物,也就是說(shuō)上一次請(qǐng)求也是408,那我們這次不再重請(qǐng)求了
    • if (retryAfter(userResponse, 0) > 0):如果服務(wù)器告訴我們了 Retry-After 多久后重試,那框架不管了。
  • HTTP_UNAVAILABLE(503):服務(wù)不可用,和408差不多,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重新請(qǐng)求 。

  • HTTP_MISDIRECTED_REQUEST(421):這個(gè)是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2連接,如果服務(wù)器返回了421,會(huì)進(jìn)行重試。

總結(jié)

需要注意是,在重定向的時(shí)候,還有這樣一段代碼:

// MAX_FOLLOW_UPS = 20
if (++followUpCount > MAX_FOLLOW_UPS) {
  throw ProtocolException("Too many follow-up requests: $followUpCount")
}

也就是說(shuō),重定向最大發(fā)生次數(shù)為20次,超過(guò)20次就會(huì)拋出異常。

這個(gè)攔截器是責(zé)任鏈中的第一個(gè),根據(jù)上一篇我們分析的,相當(dāng)于是最后一個(gè)處理響應(yīng)結(jié)果的,在這個(gè)攔截器中的主要功能就是進(jìn)行重試和重定向。

重試的前提是發(fā)生了RouteExceptionIOException,只要請(qǐng)求的過(guò)程中出現(xiàn)了這連個(gè)異常,就會(huì)通過(guò)record()方法進(jìn)行判斷是否重試。

從定向是不需要重試的情況下,根據(jù)followUpRequest()方法,判斷各種響應(yīng)碼才決定是否重定向,重定向的發(fā)生次數(shù)最大20次。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容