之前在上一篇文章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 (!forWebSocket) {
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中的字段
网友评论