OKHttp拦截器-重试和重定向拦截器

经过上一篇的解析,我们已经对OKHttp的同步请求和异步请求了然于胸,还有五大拦截器可以说是它的画龙点睛之笔,今天我们就来看看,它们是怎么运作的。

RetryAndFollowUpInterceptor,顾名思义,用来处理请求失败后重连和重定向的,上一篇我们知道了责任链调用的是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)
    }
  }
}

拦截器代码有点多,我们分步骤来看,首先是一个while死循环,因为我们出现异常后可能需要重试第二次、第三次...,所以这里用了一个死循环,将请求进行try catch捕获,如果没有异常,判断是否需要重定向,如果不需要,直接返回response,否则重新创建一个Request进行请求,并返回response;如果出现了异常,进入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进入下一次循环,循环后还是走的try语句块,这样就实现了重连机制,不用想,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, 那么不允许重试, 这个是我们创建OKHttpClient的时候进行的配置,默认为true,如果我们设置了false,就不会重试了

  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, 或者异常类型为文件未找到,就不会进行重试了,如果请求为post请求时,需要我们传递一个RequestBody对象,它是一个抽象类,isOneShot()默认返回false,如果我们需要某一个接口特殊处理,就可以重写此方法:

    class MyRequestBody : RequestBody() {
        override fun contentType(): MediaType? {
            return null
        }
    
        override fun writeTo(sink: BufferedSink) {
        }
     // 覆盖此方法,返回true,代表不要进行重试
        override fun isOneShot(): Boolean {
            return true
        }
    }
    
  3. if (!isRecoverable(e, requestSendStarted)) return false,这个方法判断一些异常类型,某些异常时不可以重试:

    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(协议异常)时,不允许重试;
    • InterruptedIOException(IO中断异常),如果是因为连接超时那么就允许重试,反之不可以;
    • SSLHandshakeException(SSL握手异常时),鉴权失败了就不可以重试;
    • SSLPeerUnverifiedException(证书过期 or 失效),不可以重试;
  4. if (!call.retryAfterFailure()) return false,判断有没有可以用来连接的路由路线,如果没有就返回false,如果存在更多的线路,那么就会尝试换条线路进行重试。

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
}

同样是调用recover()方法进行判断,这里就不多讲了。

重定向

如果请求的过程中没有抛出异常,那么就要判断是否可以重定向。

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")
}

调用followUpRequest()方法获取重定向之后的Request。

如果不允许重定向,就返回null,这时候直接把response返回即可;

如果允许重定向,获取新的请求体,判断followUpBody.isOneShot()为true,代表不可以重定向,直接返回response;

否则使用新的Request进行请求。

@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
  }
}

根据服务器响应的code判断是否进行重定向

  • HTTP_PROXY_AUTH:407 客户端使用了HTTP代理服务器,如果在请求头中添加了Proxy-Authorization,让代理服务器授权进行重定向

  • HTTP_UNAUTHORIZED:401 需要身份验证,有些服务器接口需要验证使用者身份 在请求头中添加Authorization

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

    **HTTP_TEMP_REDIRECT(307), **临时重定向

    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. 如果请求头中没有Location , 那么没办法重定向
        val location = userResponse.header("Location") ?: return null
        // 2. 解析Location请求头中的url,如果不是正确的url,返回null
        val url = userResponse.request.url.resolve(location) ?: return null
    
        // 3. 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
        val sameScheme = url.scheme == userResponse.request.url.scheme
        if (!sameScheme && !client.followSslRedirects) return null
     
        val requestBuilder = userResponse.request.newBuilder()
        // 4.判断请求是不是get或head
        if (HttpMethod.permitsRequestBody(method)) {
          val responseCode = userResponse.code
          val maintainBody = HttpMethod.redirectsWithBody(method) ||
              responseCode == HTTP_PERM_REDIRECT ||
              responseCode == HTTP_TEMP_REDIRECT
           // 5. 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式,即只有 PROPFIND 请求才能有请求体
          // HttpMethod.redirectsToGet(method) 判断是否是PROPFIND,不是返回true
          if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
            requestBuilder.method("GET", null)
          } else {
            // 如果是PROPFIND请求,添加请求体
            val requestBody = if (maintainBody) userResponse.request.body else null
            requestBuilder.method(method, requestBody)
          }
          // 6. 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding")
            requestBuilder.removeHeader("Content-Length")
            requestBuilder.removeHeader("Content-Type")
          }
        }
        // 7. 在跨主机重定向时,删除身份验证请求头
        if (!userResponse.request.url.canReuseConnectionFor(url)) {
          requestBuilder.removeHeader("Authorization")
        }
      // 返回Request对象
        return requestBuilder.url(url).build()
    }
    

    如果是以上几种状态,会走的这里的代码,并返回Request对象,其中每一步都有注释,这里就不一一赘述了。

  • HTTP_CLIENT_TIMEOUT:408,客户端请求超时,算是请求失败了,这里其实是走重试逻辑了

    • if (!client.retryOnConnectionFailure):先判断用户是否允许重试
    • if (requestBody != null && requestBody.isOneShot()):判断本次请求是否可以重试
    • if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT):如果是本身这次的响应就是重新请求的产物,也就是说上一次请求也是408,那我们这次不再重请求了
    • if (retryAfter(userResponse, 0) > 0):如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
  • HTTP_UNAVAILABLE(503):服务不可用,和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重新请求 。

  • HTTP_MISDIRECTED_REQUEST(421):这个是OKHttp4.x以后新加的,即使域名不同,OkHttp也可以合并HTTP/2连接,如果服务器返回了421,会进行重试。

总结

需要注意是,在重定向的时候,还有这样一段代码:

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

也就是说,重定向最大发生次数为20次,超过20次就会抛出异常。

这个拦截器是责任链中的第一个,根据上一篇我们分析的,相当于是最后一个处理响应结果的,在这个拦截器中的主要功能就是进行重试和重定向。

重试的前提是发生了RouteExceptionIOException,只要请求的过程中出现了这连个异常,就会通过record()方法进行判断是否重试。

从定向是不需要重试的情况下,根据followUpRequest()方法,判断各种响应码才决定是否重定向,重定向的发生次数最大20次。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容