OkHttp原始碼之RetryAndFollowUpInterceptor
之前在上一篇文章ofollow,noindex">okhttp原始碼之責任鏈模式 中有提到過,okhttp的所有功能都是通過攔截器來實現的,就是我們今天要分析的是第一個核心功能的攔截器:RetryAndFollowUpInterceptor,該攔截器主要功能是負責處理各種重新請求。
首先明確要弄明白什麼
所謂的重新請求包括:1.重定向,就是請求一個地址時,服務端返回特殊狀態碼和新的請求地址,客戶端再重新去請求這個新的地址; 2. 一些特殊情況需要重新請求 。那麼想要完成這個重新請求,我們必須搞明白以下幾點:
- 重新請求有沒有次數限制
- 哪些情況需要重新請求
- 是否每次都是新地址,若是,地址從哪裡獲取
- 重新請求和第一次請求有區別嗎
整體結構
首先我們整體看下intercept方法:
@Override public Response intercept(Chain chain) throws IOException { //省略一些程式碼 while (true) { //獲取請求結果 response = realChain.proceed(request, streamAllocation, null, null) //省略其他程式碼 Request followUp; try { //檢查這一次請求是否需要重定向到其他地址 followUp = followUpRequest(response, streamAllocation.route()); } catch (IOException e) { streamAllocation.release(); throw e; } //本次請求無需重定向,直接說明此次請求獲得了真正結果 if (followUp == null) { if (!forSocket/">WebSocket) { streamAllocation.release(); } return response; } closeQuietly(response.body()); //本次請求依然需要重定向到新的請求,檢查是否達到最大重定向次數 if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } //省略部分程式碼 request = followUp; priorResponse = response; } }
從上面的程式碼註釋中可以看到,整個流程其實很簡單,一個死迴圈在那裡執行,每次請求得到結果後,利用followUpRequest()方法看會不會返回一個新的Request,若返回需要重定向,也就是拿新的Request去獲取Response,當然這裡有一個最大重定向次數限制:
/** * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox, * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. */ private static final int MAX_FOLLOW_UPS = 20;
這裡我們解答了一個問題,那就是重定向次數限制,那麼,其他問題答案很明顯只能在followUpRequest()方法中尋找:
private Request followUpRequest(Response userResponse, Route route) throws IOException { //省略程式碼 switch (responseCode) { case HTTP_PROXY_AUTH: //省略程式碼,處理代理情況 case HTTP_UNAUTHORIZED: //省略程式碼,處理授權情況 case HTTP_PERM_REDIRECT: case HTTP_TEMP_REDIRECT: //省略程式碼,做一些檢查 // fall-through case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: //此處就是構建新的重定向請求的部分 case HTTP_CLIENT_TIMEOUT: //省略程式碼 case HTTP_UNAVAILABLE: //省略程式碼 default: return null; } }
此處我省略了大部分程式碼,只呈現出結構,這裡其實就是匹配返回的狀態碼,如果返回的是某些特殊的狀態碼,我們就重新構建一個請求,我將這些狀態碼分成4種,這四種情況都會構建一個Request,然後重新請求,不單單重定向這一種情況。
哪些要重新請求
-
HTTP_PROXY_AUTH(407)或 HTTP_UNAUTHORIZE(401)
這兩個狀態碼差不多,只不過一個是代理伺服器要驗證資訊,一個是真正伺服器要驗證資訊,我們以407為例:
case HTTP_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);
首先要檢查我們有沒有設定代理資訊,如果沒有直接拋異常,client中的proxyAuthenticator()其實獲取的是預設設定的一個proxyAuthenticator,在builder中預設的:
proxyAuthenticator = Authenticator.NONE;
這個NONE長這樣:
public interface Authenticator { /** An authenticator that knows no credentials and makes no attempt to authenticate. */ Authenticator NONE = new Authenticator() { @Override public Request authenticate(Route route, Response response) { return null; } }; /** * Returns a request that includes a credential to satisfy an authentication challenge in {@code * response}. Returns null if the challenge cannot be satisfied. */ @Nullable Request authenticate(Route route, Response response) throws IOException; }
也就是說預設的只會返回null,那麼預設情況下自然不會重新請求了,想要做到接收407狀態碼後自動重新請求,需要我們手動設定一個proxyAuthenticator,也就是覆寫authenticate方法。401狀態碼道理一樣,預設也是NONE,如果有需要,得自己手動新增授權。
-
HTTP_CLIENT_TIMEOUT (408)
408狀態碼比較特殊,okhttp直接說了這個是rare的,遇到情況很少
// 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; } if (userResponse.request().body() instanceof UnrepeatableRequestBody) { 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();
408表示請求超時,對於408,是可以拿原來的Request重新請求,但某些情況下應該放棄:
(1)上傳的request的資訊不可重複,比如從流中讀取的
(2) 之前已經遇到過408,這一次還是408,直接放棄
(3) 服務端明確告知重新嘗試時間大於0的
這種情況要說明下,收到408後,某些情況下服務端會通過在Response中加入“Retry-After"欄位告知客戶端什麼時候重新嘗試,所以這裡取了這個欄位判斷:
if (retryAfter(userResponse, 0) > 0) { return null; }
這裡如果大於0,說明要延後請求,那麼當前就不應該重新請求,所以返回null。
除了上述情況外,都會直接返回上次請求的Request然後重新請求。
-
HTTP_UNAVAILABLE(503)
503表示伺服器錯誤:
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;
通常來說遇到503不應該重新請求,但若服務端返回Retry-After欄位且為0,則這裡會重新請求,但連續兩次都收到503則會放棄。
-
重定向
這裡包括以下狀態碼:HTTP_PERM_REDIRECT(308)、 HTTP_TEMP_REDIRECT(307)、 HTTP_MULT_CHOICE(300)、 HTTP_MOVED_PERM(301)、 HTTP_MOVED_TEMP(302)、 HTTP_SEE_OTHER(303)
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, url)) { requestBuilder.removeHeader("Authorization"); } return requestBuilder.url(url).build();
這段程式碼看似很長,實際上只是找到Response中的Location欄位攜帶的新的地址,然後重新構建一個Request而已。
其他
經過上面的分析,整個RetryAndFollowUpInterceptor應該是非常清楚了,當然,這裡我們省略了一些程式碼,比如:
@Override public Response intercept(Chain chain) throws IOException { //省略其他程式碼 StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace); this.streamAllocation = streamAllocation; //省略其他程式碼 }
這個StreamAllocation是真正負責socket連線的,這裡又沒有涉及到真正的socket,為什麼在這裡例項化?這是因為RetryAndFollowUpInterceptor 裡面一些情況下是需要釋放連線的,而它又是第一個核心功能攔截器,所以必須在這裡例項化,這是為了確保這裡能釋放。
總結
現在,我們可以完整的回答之前提出的問題了
-
重新請求有沒有次數限制
有,最多20次 -
哪些情況需要重新請求
(1) 401,407:未授權情況下且自定義了授權的Authenticator
(2) 408:第一次出現,且返回的Response中Retry-After為0
(3) 503:第一次出現,且返回的Response中Retry-After為0
(4) 300,301,302,303,307,308: 自己沒有禁止okhttp重定向功能,且返回的Response重的Location欄位攜帶有效的url地址的 -
是否每次都是新地址,若是,地址從哪裡獲取
只有重定向情況會向新地址發起請求,新地址是從上一次返回的Response的header中的Location欄位攜帶的,其他情況都是向原地址重新請求 -
重新請求和第一次請求有區別嗎
401,407情況下,新的請求Request會多出授權資訊;
重定向情況下會刪掉一些多餘的Header中的欄位