背景:
某應(yīng)用需要在線上訪問外網(wǎng)url产弹,故申請(qǐng)了代理配置了okhttp的代理及鑒權(quán)。但是上線后發(fā)現(xiàn)調(diào)用外網(wǎng)url時(shí)并沒有調(diào)用成功,且報(bào)了如下異常:Too many follow-up requests:21
分析
尋找改異常的錯(cuò)誤位置:
okhttp3.internal.http.RetryAndFollowUpInterceptor#intercept 是在okhttp3的調(diào)用責(zé)任鏈中的第一個(gè)攔截器:RetryAndFollowUpInterceptor逗栽。所有攔截器按順序?yàn)?/p>
-
重試及重定向攔截器
RetryAndFollowUpInterceptor
-
橋接攔截器
BridgeInterceptor
-
緩存攔截器
CacheInterceptor
-
連接攔截器
ConnectInterceptor
-
讀寫攔截器
CallServerInterceptor
在該攔截器中祠汇,會(huì)進(jìn)行請(qǐng)求的重試和重定向仍秤。看下核心代碼可很。
//循環(huán)進(jìn)行重試诗力,直到正常返回
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
//調(diào)用責(zé)任鏈的下一個(gè)攔截器
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//路由異常捕獲處理
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
//IO異常捕獲處理
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
// 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();
}
//判斷指定response返回code需要重定向、重試的request對(duì)象我抠。
Request followUp = followUpRequest(response);
//如果發(fā)現(xiàn)需要重試的request對(duì)象為null苇本,說明不需要重試,直接返回response菜拓。
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
closeQuietly(response.body());
//否則進(jìn)入下一次循環(huán)瓣窄。進(jìn)行重試,并把重試次數(shù)+1纳鼎。如果判斷重試次數(shù)大于限制的最大重試次數(shù)(20次)俺夕。則拋出Too many follow-up requests: 21的異常,也就是我們遇到的異常
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
}
那么重點(diǎn)就在followUpRequest方法贱鄙∪懊常看一下什么情況下會(huì)返回需要重試的request。同樣貼一下核心代碼:
switch (responseCode) {
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);
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
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?
...
return requestBuilder.url(url).build();
case 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.)
...
return userResponse.request();
default:
return null;
}
這個(gè)switch中列出了各種返回狀態(tài)碼的處理邏輯逗宁,狀態(tài)碼枚舉見:java.net.HttpURLConnection映九。可以看到許多種情況下都有可能返回一個(gè)非空的request對(duì)象瞎颗,在okhttp內(nèi)部進(jìn)行自動(dòng)重試氯迂。
那怎么知道我們具體的返回結(jié)果是什么,導(dǎo)致他進(jìn)行自動(dòng)重試呢言缤?因?yàn)閛khttp除了進(jìn)行自動(dòng)重試,且超過指定次數(shù)拋出一個(gè)一句話異常外禁灼,沒有提供任何信息管挟。我們只能找別的辦法去判斷是什么導(dǎo)致了重試。
解決方法:
上文提到okhttp是用的責(zé)任鏈模型進(jìn)行的調(diào)用弄捕,共有五個(gè)默認(rèn)攔截器僻孝,且讀寫攔截器為:CallServerInterceptor。而該責(zé)任鏈?zhǔn)侵С謹(jǐn)U展的守谓,也就是說我們可以添加自己的攔截器穿铆。有兩個(gè)方法:
- addInterceptor (該方法添加的攔截器順序?yàn)樽钋懊妫簿褪钦{(diào)用時(shí)先執(zhí)行該攔截器斋荞,返回時(shí)最后經(jīng)過該攔截器)
- addNetworkInterceptor(該方法添加的是網(wǎng)絡(luò)攔截器荞雏,在非 WebSocket 請(qǐng)求時(shí)順序在 ConnectInterceptor 和 CallServerInterceptor 之間的)
由于我們的異常是在RetryAndFollowUpInterceptor中拋出的,且真正的讀寫是在CallServerInterceptor的時(shí)候,所以我們需要用addNetworkInterceptor方法添加攔截器凤优,獲取到真正返回的response悦陋。
自定義攔截器:實(shí)現(xiàn)okhttp3.Interceptor接口,重寫intercept方法筑辨。在該方法中可以用Request request = chain.request();來獲取request對(duì)象俺驶。獲取response對(duì)象需要Response response = chain.proceed(request);即調(diào)用下一個(gè)攔截器(CallServerInterceptor)獲取返回結(jié)果。
拿到返回結(jié)果后棍辕,就可以打印自己的response的code暮现、body、message等信息楚昭。來查看自己真正的返回結(jié)果是什么栖袋。
總結(jié):
個(gè)人認(rèn)為重試中的一些狀態(tài)碼會(huì)導(dǎo)致真正的錯(cuò)誤信息被隱藏,比如我遇到的407狀態(tài)碼(HTTP_PROXY_AUTH)哪替,也被自動(dòng)重試了栋荸。其實(shí)是代理的鑒權(quán)用戶名密碼不對(duì)導(dǎo)致的。但是他自動(dòng)重試后也沒有拋出407的異常凭舶,就無法直觀的發(fā)現(xiàn)真正的錯(cuò)誤晌块。造成排查困難的問題。