okhttp3的一个坑

背景:

某应用需要在线上访问外网url,故申请了代理配置了okhttp的代理及鉴权。但是上线后发现调用外网url时并没有调用成功,且报了如下异常:Too many follow-up requests:21

image.png

分析

寻找改异常的错误位置:

okhttp3.internal.http.RetryAndFollowUpInterceptor#intercept 是在okhttp3的调用责任链中的第一个拦截器:RetryAndFollowUpInterceptor。所有拦截器按顺序为

  • 重试及重定向拦截器 RetryAndFollowUpInterceptor
  • 桥接拦截器 BridgeInterceptor
  • 缓存拦截器 CacheInterceptor
  • 连接拦截器 ConnectInterceptor
  • 读写拦截器 CallServerInterceptor

在该拦截器中,会进行请求的重试和重定向。看下核心代码。

//循环进行重试,直到正常返回
    while (true) {
        if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
        }
        Response response;
        boolean releaseConnection = true;
        try {
            //调用责任链的下一个拦截器
            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对象。
        Request followUp = followUpRequest(response);
        //如果发现需要重试的request对象为null,说明不需要重试,直接返回response。
        if (followUp == null) {
            if (!forWebSocket) {
                streamAllocation.release();
            }
            return response;
        }
        closeQuietly(response.body());
        //否则进入下一次循环。进行重试,并把重试次数+1。如果判断重试次数大于限制的最大重试次数(20次)。则抛出Too many follow-up requests: 21的异常,也就是我们遇到的异常
        if (++followUpCount > MAX_FOLLOW_UPS) {
            streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
    }

那么重点就在followUpRequest方法。看一下什么情况下会返回需要重试的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;
    }

这个switch中列出了各种返回状态码的处理逻辑,状态码枚举见:java.net.HttpURLConnection。可以看到许多种情况下都有可能返回一个非空的request对象,在okhttp内部进行自动重试。

那怎么知道我们具体的返回结果是什么,导致他进行自动重试呢?因为okhttp除了进行自动重试,且超过指定次数抛出一个一句话异常外,没有提供任何信息。我们只能找别的办法去判断是什么导致了重试。

解决方法:

上文提到okhttp是用的责任链模型进行的调用,共有五个默认拦截器,且读写拦截器为:CallServerInterceptor。而该责任链是支持扩展的,也就是说我们可以添加自己的拦截器。有两个方法:

  • addInterceptor (该方法添加的拦截器顺序为最前面,也就是调用时先执行该拦截器,返回时最后经过该拦截器)
  • addNetworkInterceptor(该方法添加的是网络拦截器,在非 WebSocket 请求时顺序在 ConnectInterceptor 和 CallServerInterceptor 之间的)

由于我们的异常是在RetryAndFollowUpInterceptor中抛出的,且真正的读写是在CallServerInterceptor的时候,所以我们需要用addNetworkInterceptor方法添加拦截器,获取到真正返回的response。

自定义拦截器:实现okhttp3.Interceptor接口,重写intercept方法。在该方法中可以用Request request = chain.request();来获取request对象。获取response对象需要Response response = chain.proceed(request);即调用下一个拦截器(CallServerInterceptor)获取返回结果。

拿到返回结果后,就可以打印自己的response的code、body、message等信息。来查看自己真正的返回结果是什么。

总结:

个人认为重试中的一些状态码会导致真正的错误信息被隐藏,比如我遇到的407状态码(HTTP_PROXY_AUTH),也被自动重试了。其实是代理的鉴权用户名密码不对导致的。但是他自动重试后也没有抛出407的异常,就无法直观的发现真正的错误。造成排查困难的问题。

你可能感兴趣的:(okhttp3的一个坑)