Android 網絡全棧攻略(四)—— 從 OkHttp 攔截器來看 HTTP 協議一

上一篇我們詳解了 OkHttp 的眾多配置,本篇來看 OkHttp 是如何通過責任鏈上的內置攔截器完成 HTTP 請求與響應的,目的是更好地深入理解 HTTP 協議。這仍然是一篇偏向于協議實現向的文章,重點在于 HTTP 協議的實現方法與細節,關于責任鏈模式這些設計模式相關的內容,由于與 HTTP 協議關聯不大,因此只是有所提及但不會著重講解。

1、責任鏈與內置攔截器

不論是同步還是異步執行網絡請求,任務的執行最終都會落到 RealCall 的 getResponseWithInterceptorChain() 中:

  @Throws(IOException::class)internal fun getResponseWithInterceptorChain(): Response {// 1.構建完整的攔截器堆棧,按添加順序形成鏈式處理流程val interceptors = mutableListOf<Interceptor>()// 1.1 先添加 OkHttpClient 配置的 interceptorsinterceptors += client.interceptors// 1.2 再添加 OkHttp 內置的攔截器interceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptor// forWebSocket 用于區分是普通 HTTP 請求還是 WebSocket 握手請求。如果是普通// HTTP 請求,這里要插入 OkHttpClient 配置的網絡攔截器列表 networkInterceptorsif (!forWebSocket) {interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)// 2.創建責任鏈的初始對象val chain = RealInterceptorChain(// RealCall call = this,// 攔截器列表interceptors = interceptors,// 索引,表示當前處理的是 interceptors 中的哪一個攔截器,責任鏈的開始傳 0index = 0,exchange = null,// 請求,責任鏈的開始傳原始請求request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)// 3.開啟責任鏈的執行var calledNoMoreExchanges = falsetry {// proceed() 觸發責任鏈val response = chain.proceed(originalRequest)if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow noMoreExchanges(e) as Throwable} finally {if (!calledNoMoreExchanges) {noMoreExchanges(null)}}}

由于各個攔截器我們馬上就要逐個詳解,因此這里就簡單聊聊責任鏈的構成與執行。

被實例化的責任鏈對象 RealInterceptorChain 實現了 Interceptor.Chain 接口,該接口最重要的方法就是用于執行責任鏈的 proceed():

class RealInterceptorChain(// 實際的網絡請求對象internal val call: RealCall,// 攔截器列表private val interceptors: List<Interceptor>,// 索引,表示當前處理的是 interceptors 中的哪一個攔截器,責任鏈的開始傳 0private val index: Int,// 負責傳輸單個 HTTP 請求與響應對。在 [ExchangeCodec](處理實際 I/O 操作)的基礎上,// 實現了連接管理和事件通知的分層邏輯。internal val exchange: Exchange?,// 請求internal val request: Request,// 連接超時時間internal val connectTimeoutMillis: Int,// 讀超時internal val readTimeoutMillis: Int,// 寫超時internal val writeTimeoutMillis: Int
) : Interceptor.Chain {@Throws(IOException::class)override fun proceed(request: Request): Response {// 生成下一個責任鏈節點對象,參數 index 告知下一個責任鏈節點應該取出// interceptors[index + 1] 這個攔截器進行處理val next = copy(index = index + 1, request = request)// 取構造函傳入的 index 對應的攔截器val interceptor = interceptors[index]// 執行攔截器的處理邏輯并得到響應結果 responseval response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null")return response}
}

精簡后的 proceed() 代碼就是兩點:

  1. 生成責任鏈的下一個節點,在攔截器執行攔截邏輯的 intercept() 時作為參數傳入
  2. 執行攔截器的攔截邏輯 intercept() 得到響應結果

攔截器的攔截邏輯 intercept() 大致可以分為三部分:

  override fun intercept(chain: Interceptor.Chain): Response {// 1.獲取 Request 并根據自身功能對 Request 做出修改...Request request = chain.request();...// 2.執行參數傳入的責任鏈上下一個責任鏈節點對象 chain 的 proceed(),交接接力棒到下一個節點val networkResponse = chain.proceed(requestBuilder.build())// 3.對第 2 步得到的響應 networkResponse 根據自身功能做出修改...return networkResponse}

由于每個攔截器的功能不同,因此 1、3 兩步是否存在要看具體的攔截器功能。比如重試與重定向攔截器 RetryAndFollowUpInterceptor 就沒有第 1 步修改 Request 的需求。但所有攔截器一定都有第 2 步接力棒交接的動作,如果沒有,責任鏈就斷了,后面的攔截器就無法工作。

通過 RealInterceptorChain 的構造函數能看到,雖然它保存了所有攔截器的列表 interceptors: List<Interceptor>,但是在進行處理時,每個 RealInterceptorChain 都是根據 index 取出 interceptors 中的一個攔截器進行攔截操作的,而攔截器的攔截邏輯又會通過 proceed() 執行下一個責任鏈節點。因此可以將這種責任鏈模式看成如下結構:

2024-12-6.記憶責任鏈

因此,先添加到攔截器列表中的攔截器,就越先獲取并修改 Request,但越靠后拿到響應的 Response。

接下來我們會按照 getResponseWithInterceptorChain() 內向 interceptors 列表添加攔截器的順序逐一介紹這些攔截器。

2、重試與重定向攔截器

RetryAndFollowUpInterceptor 顧名思義就是要做重試與重定向的:

  @Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 通過參數傳入的 chain 獲取 HTTP 請求 Request 請求以及請求任務 RealCallval realChain = chain as RealInterceptorChainvar request = chain.requestval call = realChain.call// 重定向次數var followUpCount = 0// 上一次請求的響應,用于在重定向返回結果時,把上一次請求的響應體放到本次// 也就是重定向的響應體 Response 中var priorResponse: Response? = null// 是否是新的 ExchangeFinder,當不是重試且允許新路由時為 truevar newExchangeFinder = true// 可以重試的錯誤列表var recoveredFailures = listOf<IOException>()// 在滿足重試或重定向的條件時會一直循環while (true) {// 1.準備工作call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {...try {// 2.中置工作:責任鏈交接棒response = realChain.proceed(request)newExchangeFinder = true} catch (e: RouteException) { // 3.請求出錯了,進行重試判斷// 3.1 路由異常,連接失敗,請求還沒有發出去,用 recover() 判斷是否滿足重試條件if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {throw e.firstConnectException.withSuppressed(recoveredFailures)} else {// if 未命中說明滿足重試條件,那么就把這個異常放到 recoveredFailures 里面recoveredFailures += e.firstConnectException}// 由于滿足重試條件要進行重試了,因此要把 newExchangeFinder 置為 false 了newExchangeFinder = false// 由于本次循環失敗但滿足重試條件,因此跳出本次 while 循環,重試進入下一次循環continue} catch (e: IOException) {// 3.2 請求可能已經發出,但與服務器通信失敗,還是用 recover() 判斷能否重試if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {throw e.withSuppressed(recoveredFailures)} else {recoveredFailures += e}newExchangeFinder = falsecontinue}// 4.代碼能執行到這里說明前面沒出錯,不用進行重試,那么就開始做后置工作,// 根據返回的 Response 響應判斷是否需要做重定向工作了// 4.1 如果之前做過重定向,那么 priorResponse 就不為空,將其放到本次響應體中if (priorResponse != null) {response = response.newBuilder().priorResponse(priorResponse.newBuilder().body(null).build()).build()}// 4.2 檢查是否需要進行重定向,如果 followUp 為空則不需要val exchange = call.interceptorScopedExchangeval followUp = followUpRequest(response, exchange)if (followUp == null) {if (exchange != null && exchange.isDuplex) {call.timeoutEarlyExit()}closeActiveExchange = falsereturn response}// 4.3 需要重定向的情況val followUpBody = followUp.body// 如果請求的 Request 有請求體,并且請求體中配置了只允許傳輸一次,那就不做重定向直接返回if (followUpBody != null && followUpBody.isOneShot()) {closeActiveExchange = falsereturn response}response.body?.closeQuietly()// 重定向次數超過 MAX_FOLLOW_UPS(默認 20),也不做重定向if (++followUpCount > MAX_FOLLOW_UPS) {throw ProtocolException("Too many follow-up requests: $followUpCount")}// 將重定向請求賦值給 request 作為下一次循環的請求,從而實現重定向請求// response 賦值給 priorResponse 作為下一次請求的前置請求request = followUppriorResponse = response} finally {// 5、與 1 的準備工作相對應,現在本次請求結束了,要做收尾工作call.exitNetworkInterceptorExchange(closeActiveExchange)}}}

RetryAndFollowUpInterceptor 的 intercept() 如果細致劃分的話,可以像注釋中標記的那樣分成 5 步。

第 1 步準備工作與第 5 步收尾工作可以放在一起看,調用的都是 RealCall 的方法:

  /*** 為可能遍歷所有網絡攔截器的流程做準備。此操作將嘗試找到一個 Exchange 來承載請求。* 如果請求已被緩存滿足,則不需要 Exchange。** @param newExchangeFinder 如果這不是一次重試且允許執行新路由,則為 true。*/fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {// 確保當前沒有已綁定的 Exchangecheck(interceptorScopedExchange == null)// 如果上一個請求體與響應體未關閉則拋異常,確保前一次請求的資源已釋放,防止資源泄漏synchronized(this) {check(!responseBodyOpen) {"cannot make a new request because the previous response is still open: " +"please call response.close()"}check(!requestBodyOpen)}// 創建新的路由查找器(如非重試場景)if (newExchangeFinder) {this.exchangeFinder = ExchangeFinder(connectionPool,createAddress(request.url),this,eventListener)}}/*** @param closeExchange 是否應關閉當前 Exchange(通常因異常或重試導致不再使用)*/internal fun exitNetworkInterceptorExchange(closeExchange: Boolean) {// 線程安全校驗:確保當前調用未被釋放synchronized(this) {check(expectMoreExchanges) { "released" }}// 強制關閉當前 Exchange(如發生異常需要重試)if (closeExchange) {exchange?.detachWithViolence()}// 清理當前攔截器作用域下的 ExchangeinterceptorScopedExchange = null}

第 2 步執行下一個責任鏈,前面已經分析過。比較重要的是第 3 步重試與第 4 步重定向,我們詳細介紹一下。

2.1 重試

從注釋的 3.1 與 3.2 兩步可以看出,允許進行重試判斷的情況有兩種:

  1. 拋出 RouteException,路由異常,連接失敗,請求未能發出
  2. 拋出 IOException,請求可能已經發出,但與服務器通信失敗

只有在這兩種情況下才能使用 recover() 做進一步的細分判斷是否能重試:

  /*** 報告并嘗試從與服務器通信的失敗中恢復。若異常 `e` 可恢復則返回 true,否則返回 false。* 注意:帶請求體的請求僅在以下情況可恢復:* 1. 請求體已被緩沖;* 2. 失敗發生在請求發送前。*/private fun recover(e: IOException,call: RealCall,userRequest: Request,requestSendStarted: Boolean): Boolean {// 情況 1:應用層禁止重試(通過 OkHttpClient 配置)if (!client.retryOnConnectionFailure) return false// 情況 2:請求已發送且為一次性請求體(無法重試發送 Body)if (requestSendStarted && requestIsOneShot(e, userRequest)) return false// 情況 3:異常不可恢復(如 SSL 證書錯誤)if (!isRecoverable(e, requestSendStarted)) return false// 情況 4:無更多可用路由(如所有 IP 嘗試失敗)if (!call.retryAfterFailure()) return false// 所有條件通過,允許恢復并重試return true}

下面看每種情況的具體內容。

客戶端是否允許重試

取決于 OkHttpClient 的 retryOnConnectionFailure 屬性,默認值為 true,可以通過 Builder 模式配置該屬性決定是否允許由該 OkHttpClient 發送的所有 Request 進行重試:

  @get:JvmName("retryOnConnectionFailure") val retryOnConnectionFailure: Boolean =builder.retryOnConnectionFailure

單個 Request 是否允許重試

對于已經發送的一次性請求體是無法重試的,它取決于 requestSendStarted 與 requestIsOneShot() 兩部分。

對于 requestSendStarted,我們看注釋 3.1 與 3.2 傳的是不一樣的:

  • 當拋出 RouteException 時,這時由于路由問題并沒有連接到服務器,可以斷定請求一定沒有被發送,所以直接給 requestSendStarted 傳了 false
  • 當拋出 IOException 時,只有在這個異常的具體類型是 ConnectionShutdownException 時才認為由于連接中斷沒有發出請求,其他的情況都認為請求已發出。由于只有 HTTP2 才會拋出這個異常,所以對于 HTTP1 而言,requestSendStarted 一定是 true

再看 requestIsOneShot():

  private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {val requestBody = userRequest.bodyreturn (requestBody != null && requestBody.isOneShot()) ||e is FileNotFoundException}

兩個判斷條件:

  1. 如果有請求體,且是一次性請求體
  2. 發生 FileNotFoundException,即請求體依賴文件,但該文件不存在

第一個條件,RequestBody 的 isOneShot() 是一個默認值為 false 的 open 方法,也就是說默認情況下,請求不是一次性的。但如果需要配置為 OneShot 的話,可以通過重寫該函數實現。

第二個條件,假設一個 HTTP 請求的 Body 是基于本地文件的(比如上傳文件):

val file = File("image.jpg")
val request = Request.Builder().url("https://api.example.com/upload").post(file.asRequestBody("image/jpeg".toMediaType())).build()

如果因為文件不存在導致 FileNotFoundException,重試也還是相同的結果,因此這種情況也不能重試。

是否為可重試的異常類型

某些異常發生時,是不允許重試的,因為這些異常是真的因為客戶端或服務端的代碼有問題,即便重試無數次得到的也都是錯誤結果;而有些異常可能是由于網絡波動導致異常發生,換個路線重試可能就會解決問題。因此需要 isRecoverable() 判斷,哪些異常可以重試,哪些不可以:

  private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {// 1.協議異常,不重試if (e is ProtocolException) {return false}// 2.一般的 IO 中斷異常不會重試,但是如果是 Socket 超時異常,有可能是網絡波動造成的,// 可以嘗試其它路線(如果有)if (e is InterruptedIOException) {return e is SocketTimeoutException && !requestSendStarted}// 3.SSL 握手異常,如果是證書出現問題,不能重試if (e is SSLHandshakeException) {// 由 X509TrustManager 拋出的 CertificateException,不重試if (e.cause is CertificateException) {return false}}// 4.SSL 未授權異常(證書校驗失敗,不匹配或過期等問題)不重試  if (e is SSLPeerUnverifiedException) {// 這里框架作者舉的例子是 CertificatePinning 的錯誤,我們在上一篇詳細講過// e.g. a certificate pinning error.return false}return true}

四個判斷條件中,后兩條是證書驗證相關的,這部分在上一篇講 OkHttpClient 的配置時有講過,比如 CertificatePinning 的使用、X509 證書驗證等等,所以不再贅述。

第 2 條也比較好理解,如果是網絡波動造成 Socket 連接超時,重試時可能會躲過網絡波動,這時允許重試是合理的。

需要稍作解釋的是第 1 條 —— 協議異常。

ProtocolException 是一種 IOException,比如 OkHttp 的 CallServerInterceptor 在其處理責任鏈的 intercept() 內就拋出了這種異常:

  @Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {try {...var code = response.code...if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {throw ProtocolException("HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")}return response}}

看條件,是狀態碼為 204 或 205 且響應體內容長度大于 0 時會拋出 ProtocolException。兩個狀態碼的含義分別為:

  • 204:No Content,表示無內容。服務器成功處理,但未返回內容。在未更新網頁的情況下,可確保瀏覽器繼續顯示當前文檔
  • 205:Reset Content,表示重置內容。服務器處理成功,用戶終端(例如:瀏覽器)應重置文檔視圖。可通過此返回碼清除瀏覽器的表單域

也就是說,狀態碼告訴我們服務器沒有返回內容,即沒有響應體,但是我們通過代碼拿到響應體的內容長度大于 0,這兩個條件前后矛盾,有可能是服務器響應本身就存在問題,就算重試也還是得到一個錯誤結果,于是拋出 ProtocolException 不進行重試。

是否有其他可用路由

我們在配置 OkHttpClient 的時候可能會為客戶端配置多個代理,而服務器端的同一個域名也可能會有多個 IP 地址,當某條路線通信失敗后,如果存在更多路線,就會換個路線嘗試。比如說,restapi.amap.com 解析為 IP1 和 IP2,如果 IP1 通信失敗會重試 IP2。

最后我們用一張圖來總結一下重試的流程:

2.2 重定向

在進行重定向判斷時,框架是將響應 Response 與 Exchange 對象傳入 followUpRequest() 得到了一個新的 Request 請求 followUp。在 followUp 不為空且其 body 不為空、followUp 不是一次性 Request 以及重定向次數未超過閾值的情況下,才會進行重定向。那么 followUpRequest() 就是一個很關鍵的方法:

  /*** 根據接收到的 [userResponse] 確定要進行的 HTTP 請求。這將添加身份驗證頭、跟隨重定向或處理* 客戶端請求超時。如果后續請求不必要或不適用,則返回 null。*/@Throws(IOException::class)private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {// 獲取服務器響應的狀態碼val route = exchange?.connection?.route()val responseCode = userResponse.codeval method = userResponse.request.methodwhen (responseCode) {// 407 代理認證:客戶端使用了 HTTP 代理,需要在請求頭中添加【Proxy-Authorization】,// 讓代理服務器授權HTTP_PROXY_AUTH -> {val selectedProxy = route!!.proxyif (selectedProxy.type() != Proxy.Type.HTTP) {throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")}// 調用 OkHttpClient 代理的鑒權接口return client.proxyAuthenticator.authenticate(route, userResponse)}// 401 未授權:有些服務器接口需要驗證使用者身份,要在請求頭中添加【Authorization】HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)// HTTP_PERM_REDIRECT:308 永久重定向、HTTP_TEMP_REDIRECT:307 臨時重定向、// HTTP_MULT_CHOICE:300 多種選擇、HTTP_MOVED_PERM:301 永久移動、// HTTP_MOVED_TEMP:302 臨時移動、HTTP_SEE_OTHER:303:查看其它地址,這些情況構建重定向請求HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {return buildRedirectRequest(userResponse, method)}// 408:服務器等待客戶端發送的請求時間過長,超時。實際中用的很少,像 HAProxy 這種服務器// 會用這個狀態碼,不需要修改請求,只需再申請一次即可。HTTP_CLIENT_TIMEOUT -> {// 假如應用層(指 OkHttpClient)配置了不允許重試就返回 nullif (!client.retryOnConnectionFailure) {return null}// 如果是一次性 RequestBody,不重試val requestBody = userResponse.request.bodyif (requestBody != null && requestBody.isOneShot()) {return null}// 如果上一次響應的狀態碼就是 408,不重試val priorResponse = userResponse.priorResponseif (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}// 503:服務不可用。由于超載或系統維護,服務器暫時的無法處理客戶端的請求。// 延時的長度可包含在服務器的 Retry-After 頭信息中HTTP_UNAVAILABLE -> {val priorResponse = userResponse.priorResponse// 如果上一次響應的狀態碼也是 503 則不重試if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {return null}// 如果服務器返回的 Retry-After 是 0,也就是立即重試的意思,框架才重新請求if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {return userResponse.request}return null}// 421:錯誤連接。HTTP/2 允許不同域名的請求共享連接(看 RealConnection.isEligible()),// 但若服務器返回 421,需斷開合并連接。HTTP_MISDIRECTED_REQUEST -> {val requestBody = userResponse.request.body// 一次性請求體不重試if (requestBody != null && requestBody.isOneShot()) {return null}// 無關聯的 Exchange 對象(表示無實際網絡交互)或者當前連接未啟用合并機制,不重試if (exchange == null || !exchange.isCoalescedConnection) {return null}// 標記連接禁止合并,禁止后續請求復用該連接進行合并(將 noCoalescedConnections 置位)exchange.connection.noCoalescedConnections()// 返回原請求重試return userResponse.request}else -> return null}}

可以看出,followUpRequest() 會根據不同的響應狀態碼做出判斷是否進行重定向,如需要進行重定向就返回 Request 否則返回 null。

下面我們按照狀態碼分類介紹上述狀態碼的處理過程。

3xx

followUpRequest() 將所有 3xx 狀態碼都放在一個 case 中處理了,就是用 buildRedirectRequest() 創建一個重定向請求:

  private fun buildRedirectRequest(userResponse: Response, method: String): Request? {// 如果 OkHttpClient 配置了不允許重定向則返回 nullif (!client.followRedirects) return null// 檢查響應頭中是否有 Location 字段,該字段的值能否解析成 URL,// 可以則解析并保存 URL,否則返回 null 不進行重定向val location = userResponse.header("Location") ?: return nullval url = userResponse.request.url.resolve(location) ?: return null// 檢查重需要定向的 URL 與此前請求的 URL 的 scheme 是否相同。scheme 在 OkHttp 這個// 框架中就只有 http 或 https 兩個值。假如 OkHttpClient 配置 followSslRedirects 為// false,那就不允許在 SSL(HTTPS)與非 SSL(HTTP)協議之間重定向val sameScheme = url.scheme == userResponse.request.url.schemeif (!sameScheme && !client.followSslRedirects) return null// 大多數重定向請求都沒有請求體val requestBuilder = userResponse.request.newBuilder()// 不是 GET 或 HEAD 方法的話,是允許有請求體的if (HttpMethod.permitsRequestBody(method)) {val responseCode = userResponse.code// 如果請求方法是 PROPFIND 或者響應狀態碼為 308、307,需要保持請求體val maintainBody = HttpMethod.redirectsWithBody(method) ||responseCode == HTTP_PERM_REDIRECT ||responseCode == HTTP_TEMP_REDIRECT// 如果請求方法不是 PROPFIND 且響應狀態碼不是 308、307,重定向方法要使用 GETif (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {requestBuilder.method("GET", null)} else {// 如果請求方法是 PROPFIND 或者響應狀態碼是 307、308 之一,則使用原請求方法與請求體val requestBody = if (maintainBody) userResponse.request.body else nullrequestBuilder.method(method, requestBody)}// 如果不需要保持請求體就移除以下請求頭if (!maintainBody) {requestBuilder.removeHeader("Transfer-Encoding")requestBuilder.removeHeader("Content-Length")requestBuilder.removeHeader("Content-Type")}}// 當重定向到不同主機時,移除 Authorization 頭if (!userResponse.request.url.canReuseConnectionFor(url)) {requestBuilder.removeHeader("Authorization")}return requestBuilder.url(url).build()}

buildRedirectRequest() 可以大致分為兩部分:

  1. 檢查是否滿足重定向條件,不滿足則返回 null,涉及到的不滿足情況包括:
    • OkHttpClient 的重定向配置 followRedirects 設置為 false
    • 返回的響應體是 3xx 要求重定向,但響應頭中沒有 Location 或者 Location 的值無法解析為 URL
    • 原請求的 URL 與原請求對應的響應頭中 Location 指定的 URL 發生了 SSL 協議切換(對于 OkHttp 框架而言,就是一個 URL 是 HTTP,另一個 URL 是 HTTPS),但 OkHttpClient 又關閉了跨 SSL 重定向的配置 followSslRedirects
  2. 滿足重定向條件,就構造重定向的請求頭與請求體:
    • 對于沒有請求體的 GET 與 HEAD 方法而言,只需要額外再判斷一個跨主機重定向時去掉 Authorization 請求頭即可
    • 對于其他可以有請求體的方法,先判斷是否保持原請求的請求體 maintainBody,對于明確需要保持的方法 PROPFIND(目前 redirectsWithBody() 只有傳入 PROPFIND 才返回 true)或者狀態碼為 307、308 時,maintainBody 為 true
    • 隨后決定重定向請求使用哪個方法以及請求體。如果原請求方法不是 PROPFIND(目前 redirectsToGet() 除了 PROPFIND 以外其余都是 true),且狀態碼不是 307 與 308,那么重定向請求的方法就是 GET 且沒有方法體;否則重定向方法就是 PROPFIND 且請求體為原請求的請求體

為什么 307、308 以及 PROPFIND 需要做出特殊的判斷與處理,強制保留原請求的方法和請求體?

其中 307、308 是 HTTP 重定向規范(RFC 7231 (HTTP/1.1) 和 RFC 7538 (HTTP/308))中規定的,要求客戶端在重定向時保持原始請求方法和請求體。

至于 PROPFIND 是 WebDAV 協議的擴展方法,行為類似于標準 HTTP 的 GET,但需要攜帶 XML 請求體以描述查詢條件。這個方法本身就需要保持請求體,因為服務器可能依賴請求體中的 XML 內容處理查詢。

因此關于重定向的處理思路可以總結為如下表格:

條件操作示例場景
狀態碼為307/308強制保留方法和請求體服務器要求保持原始請求
方法為PROPFIND/POST/PUT保留請求體(需配合307/308)WebDAV查詢或表單提交
其他狀態碼(301/302)改為GET并移除請求體傳統重定向邏輯

4xx

我們先說兩個比較相近的:HTTP_UNAUTHORIZED(401 未授權)與 HTTP_PROXY_AUTH(407 代理認證)。

實際上這兩個狀態碼我們在上一篇講 OkHttp 配置時已經講過:當你要訪問的服務器資源需要授權才能訪問,而你在請求中又沒有攜帶授權信息相關的請求頭時,服務器會返回 401 狀態碼;407 是類似的,當你使用代理服務器又沒有提供鑒權請求頭時服務器就會返回 407。

清楚原因后,對于兩種狀態碼的處理方案也就呼之欲出了,就是在請求時帶上授權信息唄。按照 OkHttp 框架給出的,對于 401 調用 OkHttpClient 的 authenticator 的 authenticate() 進行授權,對于 407 則調用 OkHttpClient 的 proxyAuthenticator 的 authenticate() 授權。但 authenticator 與 proxyAuthenticator 的默認值都是 Authenticator 接口的默認實現 Authenticator.NONE:

fun interface Authenticator {@Throws(IOException::class)fun authenticate(route: Route?, response: Response): Request?companion object {/** An authenticator that knows no credentials and makes no attempt to authenticate. */@JvmFieldval NONE: Authenticator = AuthenticatorNone()private class AuthenticatorNone : Authenticator {override fun authenticate(route: Route?, response: Response): Request? = null}}
}

因此通過這種方式需要自己實現 Authenticator 接口:

val client = OkHttpClient.Builder().authenticator(object : Authenticator {override fun authenticate(route: Route?, response: Response): Request? {// 添加 Authorization 與 Proxy-Authorization 請求頭return response.request.newBuilder().header("Authorization", "Bearer xxxxx....").header("Proxy-Authorization", "xxxxx....").build()}})

然后說一下 HTTP_CLIENT_TIMEOUT(408),是服務器等待客戶端發送超時。這個實際上跟重定向沒啥關系,屬于超時重試的邏輯,在刨除不滿足重試的條件后,無需修改請求進行重試即可。我們來看看它這種情況下用到的 retryAfter():

  private fun retryAfter(userResponse: Response, defaultDelay: Int): Int {val header = userResponse.header("Retry-After") ?: return defaultDelayif (header.matches("\\d+".toRegex())) {return Integer.valueOf(header)}return Integer.MAX_VALUE}

根據響應頭中的 Retry-After 字段,返回當前距離下一次重試需要的時間。當前會忽略 HTTP 的日期,并且假設任何 int 型的非 0 值都是延遲。

實際上就是返回一個延遲的值,表示過多長時間之后再重試。

5xx

在 followUpRequest() 中只有 HTTP_UNAVAILABLE(503 服務不可用)一個 5 開頭的狀態碼,它是指由于超載或系統維護,服務器暫時的無法處理客戶端的請求。需要在上一次響應不是 HTTP_UNAVAILABLE 的情況下通過 retryAfter() 判斷是否可以立即進行重試,如果服務器返回的 Retry-After 響應頭的值不為 0,那么就放棄重試。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/84143.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/84143.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/84143.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

免費AI工具整理

1、NVIDIA models ALL&#xff1a;Try NVIDIA NIM APIs example&#xff1a;llama-3.1-405b-instruct Model by Meta | NVIDIA NIM 2、文心一言 文心一言 3、納米AI 納米AI搜索 4、其他 ChatGPT 鏡像網址&#xff08;5月持續更新&#xff09; - 最優網址

C++ std::find() 函數全解析

std::find()是C標準庫中用于線性查找的基礎算法&#xff0c;屬于<algorithm>頭文件&#xff0c;可應用于任何支持迭代器的容器。 一、函數原型與參數 template< class InputIt, class T > InputIt find( InputIt first, InputIt last, const T& value );??…

MySQL--day6--單行函數

&#xff08;以下內容全部來自上述課程&#xff09; 單行函數 1. 內置函數及分類 單行函數聚合函數&#xff08;或分組函數&#xff09; 1.1 單行函數特點 操作數據對象接受參數返回一個結果只對一行進行變換每行返回一個結果可以嵌套參數可以是一列或一個值 2. 數值函…

GO語言學習(九)

GO語言學習&#xff08;九&#xff09; 上一期我們了解了實現web的工作中極為重要的net/http抱的細節講解&#xff0c;大家學會了實現web開發的一些底層基礎知識&#xff0c;在這一期我來為大家講解一下web工作的一個重要方法&#xff0c;&#xff1a;使用數據庫&#xff0c;現…

解決MybatisPlus使用Druid1.2.11連接池查詢PG數據庫報Merge sql error的一種辦法

目錄 前言 一、問題重現 1、環境說明 2、重現步驟 3、錯誤信息 二、關于LATERAL 1、Lateral作用場景 2、在四至場景中使用 三、問題解決之道 1、源碼追蹤 2、關閉sql合并 3、改寫處理SQL 四、總結 前言 在博客&#xff1a;【寫在創作紀念日】基于SpringBoot和PostG…

嵌入式學習--江協51單片機day8

這個本來應該周末寫的&#xff0c;可是一直想偷懶&#xff0c;只能是拖到周一了&#xff0c;今天把51結個尾&#xff0c;明天開始學32了。 學習內容LCD1602&#xff0c;直流電機&#xff0c;AD/DA&#xff0c;紅外遙控 LCD1602 內部的框架結構 屏幕小于數據顯示區&#xff…

HUAWEI華為MateBook D 14 2021款i5,i7集顯非觸屏(NBD-WXX9,NbD-WFH9)原裝出廠Win10系統

適用型號&#xff1a;NbD-WFH9、NbD-WFE9A、NbD-WDH9B、NbD-WFE9、 鏈接&#xff1a;https://pan.baidu.com/s/1qTCbaQQa8xqLR-4Ooe3ytg?pwdvr7t 提取碼&#xff1a;vr7t 華為原廠WIN系統自帶所有驅動、出廠主題壁紙、系統屬性聯機支持標志、系統屬性專屬LOGO標志、Office…

【Python】Python 裝飾器的用法總結

在 Python 中&#xff0c;裝飾器&#xff08;Decorator&#xff09; 是一種設計模式&#xff0c;用于在不修改函數或類代碼的情況下動態地擴展其功能。裝飾器廣泛應用于日志記錄、性能監控、權限驗證等場景&#xff0c;提供了一種簡潔優雅的方式來“包裹”現有的代碼。本文將介…

【C++】控制臺小游戲

移動&#xff1a;W向上&#xff0c;S上下&#xff0c;A向左&#xff0c;D向右 程序代碼&#xff1a; #include <iostream> #include <conio.h> #include <windows.h> using namespace std;bool gameOver; const int width 20; const int height 17; int …

「MATLAB」計算校驗和 Checksum

什么是校驗和 是一個算法&#xff0c;將一串數據累加&#xff0c;得到一個和。 MATLAB程序 function c_use Checksum(packet) %Checksum 求校驗和 % 此處checksum提供詳細說明checksum 0;for i 1:length(packet)value hex2dec(packet(i));checksum checksum value; …

JavaScript面試題之消息隊列

JavaScript消息隊列詳解&#xff1a;單線程的異步魔法核心 在JavaScript的單線程世界中&#xff0c;消息隊列&#xff08;Message Queue&#xff09;是實現異步編程的核心機制&#xff0c;它像一位高效的調度員&#xff0c;讓代碼既能“一心多用”又避免卡頓。本文將深入剖析消…

京東外賣分潤系統部署實操!0門檻入駐+全平臺接入+自定義比例...這些人,賺翻了!

隨著京東外賣的發展勢頭日漸迅猛&#xff0c;許多創業者們的態度也逐漸從原本的觀望轉變為了切實的行動&#xff0c;并開始通過各個渠道詢問起了京東外賣自動分潤系統部署相關的各項事宜&#xff0c;連帶著以京東外賣自動分潤系統質量哪家強為代表的多個問題&#xff0c;也成為…

【辦公類-18-06】20250523(Python)“口腔檢查涂氟信息”批量生成打印(學號、姓名、學校、班級、身份證、戶籍、性別、民族)

背景需求: 6月是常規體檢,前幾天發了體檢表(驗血單),用Python做了姓名等信息的批量打印 【辦公類-18-04】20250520(Python)“驗血單信息”批量生成打印(學校、班級、姓名、性別)-CSDN博客文章瀏覽閱讀969次,點贊19次,收藏11次。【辦公類-18-04】20250520(Python)…

Python郵件處理:POP與SMTP

poplib簡介 poplib 是Python 3中的官方郵件庫&#xff0c;實現了POP的標準&#xff1a;RFC1939&#xff0c;用于郵件的收取。與之類似的還有imaplib 。 &#xff08;注&#xff1a;本文僅拿pop舉例&#xff09; poplib的使用方法&#xff0c;就是幾步&#xff1a; 先創建一…

IP風險度自檢,多維度守護網絡安全

如今IP地址不再只是網絡連接的標識符&#xff0c;更成為評估安全風險的核心維度。IP風險度通過多維度數據建模&#xff0c;量化IP地址在網絡環境中的安全威脅等級&#xff0c;已成為企業反欺詐、內容合規、入侵檢測的關鍵工具。據Gartner報告顯示&#xff0c;2025年全球78%的企…

Flink集成資源管理器

Flink集成資源管理器 Apache Flink 支持多種資源管理器&#xff0c;主要包括以下幾種?&#xff1a; YARN ResourceManager ?&#xff1a;適用于使用 Hadoop YARN 作為資源管理器的環境。YARN ResourceManager 負責管理集群中的資源&#xff0c;包括 CPU、內存等&#xff0c;并…

upload 文件上傳審計

目錄 LOW Medium HIgh Impossible 概述 很多Web站點都有文件上傳的接口&#xff08;比如注冊時上傳頭像等&#xff09;&#xff0c;由于沒有對上傳的文件類型進行嚴格限制&#xff0c;導致可以上傳一些文件&#xff08;比如Webshell&#xff09;。 上傳和SQL、XSS等都是主流…

【freertos-kernel】list

freertos list 基本類型結構體ListItem_t &#xff08;list.h&#xff09;List_t &#xff08;list.h&#xff09; 宏函數函數vListInitialisevListInitialiseItemvListInsertEndvListInsertuxListRemove 基本類型 freertos為了兼容性&#xff0c;重新定義了基本類型&#xff…

游戲盾的功有哪些?

游戲盾的功能主要包括以下幾方面&#xff1a; 一、網絡攻擊防護 DDoS攻擊防護&#xff1a; T級防御能力&#xff1a;游戲盾提供分布式云節點防御集群&#xff0c;可跨地區、跨機房動態擴展防御能力和負載容量&#xff0c;輕松達到T級別防御&#xff0c;有效抵御SYN Flood、UD…

PycharmFlask 學習心得:路由(3-4)

對路由的理解&#xff1a; 用戶輸入網址 例如&#xff1a;http://localhost:5000/hello 瀏覽器會向這個地址發起一個 HTTP 請求&#xff08;比如 GET 請求&#xff09; 請求到達 Flask 的服務器 Flask 監聽著某個端口&#xff08;如 5000&#xff09;&#xff0c;收到請求后…