okhttp3的一個(gè)坑

背景:

某應(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

image.png

分析

尋找改異常的錯(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ò)誤晌块。造成排查困難的問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帅霜,一起剝皮案震驚了整個(gè)濱河市匆背,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌身冀,老刑警劉巖钝尸,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異搂根,居然都是意外死亡珍促,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門剩愧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猪叙,“玉大人,你說我怎么就攤上這事仁卷⊙妫” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵锦积,是天一觀的道長(zhǎng)芒帕。 經(jīng)常有香客問我,道長(zhǎng)丰介,這世上最難降的妖魔是什么背蟆? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任鉴分,我火速辦了婚禮,結(jié)果婚禮上淆储,老公的妹妹穿的比我還像新娘冠场。我一直安慰自己,他們只是感情好本砰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布碴裙。 她就那樣靜靜地躺著,像睡著了一般点额。 火紅的嫁衣襯著肌膚如雪舔株。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天还棱,我揣著相機(jī)與錄音载慈,去河邊找鬼。 笑死珍手,一個(gè)胖子當(dāng)著我的面吹牛办铡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琳要,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼寡具,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稚补?” 一聲冷哼從身側(cè)響起童叠,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎课幕,沒想到半個(gè)月后厦坛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乍惊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年杜秸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片润绎。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡亩歹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凡橱,到底是詐尸還是另有隱情,我是刑警寧澤亭姥,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布稼钩,位于F島的核電站,受9級(jí)特大地震影響达罗,放射性物質(zhì)發(fā)生泄漏坝撑。R本人自食惡果不足惜静秆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巡李。 院中可真熱鬧抚笔,春花似錦、人聲如沸侨拦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)狱从。三九已至膨蛮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間季研,已是汗流浹背敞葛。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留与涡,地道東北人惹谐。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像驼卖,于是被迫代替她去往敵國(guó)和親氨肌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349