探索OkHttp系列 (二) 错误重试与重定向
前言
在上一篇文章「探索OkHttp系列 (一) 请求的发起与响应」,我们介绍了请求的发起与响应的整个过程,在介绍请求响应的时候,最关键的点是拦截器机制与责任链模式,关于责任链模式在请求的响应中是如何运用的,我们已经在上篇文章讲述了,但是上篇文章没有去详细地介绍各个拦截器的作用,从这篇文章开始,我们就一一地介绍各个拦截器。
拦截器的工作流程如下:
如果用户没有加入自定义的拦截器,那么 RetryAndFollowUpInterceptor
就是我们的责任链中最先被调用的拦截器,我们这篇文章就从RetryAndFollowUpInterceptor
拦截器开始介绍,该拦截器作用是进行错误重试和重定向。
Http协议中的重定向
我们先了解下Http
协议中的重定向知识。
Http
协议中,当客户端请求的资源的位置在服务器中被转移,服务器就会发送一个响应报文,该响应报文的Response Code
为3XX
,表示客户端需要「重定向」请求,并且服务端会在该响应报文的Response Header
的Location
字段中放入新的URL
,这样客户端就可以向该 Location
字段所指定的 URL
重新请求从而得到需要的数据。
该过程如下:
关于重定向状态码3XX
的详细资料 可以查看:HTTP状态码 - 维基百科.
重定向分类:
- 永久重定向:表示重定向操作是永久的,原URL应不再被使用,优先选用新的
URL
。 - 临时重定向:有时候请求的资源无法从其标准地址访问,但是却可以从另外的地方访问。在这种情况下可以使用临时重定向。搜索引擎不会记录该新的、临时的链接。
- 特殊重定向(状态码
304
,其实与重定向无关):判断资源缓存是否有效等。
重定向带来的相关问题:
- 性能耗损:多了一次请求与应答
- 循环跳转:可能出现重定向循环跳转
RetryAndFollowUpInterceptor
intercept
我们从RetryAndFollowUpInterceptor
的intercept
方法开始,来探究RetryAndFollowUpInterceptor
的工作机制。
RetryAndFollowUpInterceptor::intercept
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
// 获取请求Request
var request = chain.request
val call = realChain.call
// 重定向的次数
var followUpCount = 0
// 上一次请求返回的Response
var priorResponse: Response? = null
var newExchangeFinder = true
var recoveredFailures = listOf<IOException>()
// 注意这里是一个死循环
while (true) {
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
var response: Response
var closeActiveExchange = true
try {
// 处理取消事件
if (call.isCanceled()) {
throw IOException("Canceled")
}
try {
// 调用拦截器责任链,获取下一个拦截器返回的Response
response = realChain.proceed(request)
newExchangeFinder = true
} catch (e: RouteException) {
// The attempt to connect via a route failed. The request will not have been sent.
// 路由连接失败
// 检测是否可以从失败中恢复,如果可以恢复就返回true,就会在下面continue,进行重试;
// 不可以恢复就抛出异常
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e.firstConnectException
}
newExchangeFinder = false
continue
} catch (e: IOException) {
// An attempt to communicate with a server failed. The request may have been sent.
// IO过程中,抛出异常
// 检测是否可以从失败中恢复,如果可以恢复就返回true,就会在下面continue,进行重试;
// 不可以恢复就抛出异常
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newExchangeFinder = false
continue
}
// Attach the prior response if it exists. Such responses never have a body.
// 如果上一次请求priorResponse存在,就在本次Response中设置上一次的priorResponse,
// 设置的priorResponse的body为空
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}
val exchange = call.interceptorScopedExchange
// 根据Response Code,尝试获取「重试」或「重定向」对应的Request
val followUp = followUpRequest(response, exchange)
// 获取不到Request,则停止计时,并且返回Response
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
val followUpBody = followUp.body
// followUpBody.isOneShot():该方法默认返回false,如果用户希望Request的Body最多传输
// 一次,那么该方法就需要返回true。
// 这里的意思是:如果用户只希望Request的Body最多传输一次,那么这里就不再进行请求,而是直接
// 返回Response。
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}
response.body?.closeQuietly()
// 「重试」、「重定向」次数超过最大值,抛出异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
// 记录要重新发送的请求
request = followUp
// 记录本次请求拿到的Response
priorResponse = response
} finally {
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
接着我们看看followUpRequest
方法,是如何 尝试获取「重试」或「重定向」对应的Request
。
followUpRequest
RetryAndFollowUpInterceptor::followUpRequest
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
val route = exchange?.connection?.route()
// 获取响应码
val responseCode = userResponse.code
// 获取请求方法
val method = userResponse.request.method
// 根据不同的响应码,做不同的处理
when (responseCode) {
// Response Code : 407
// 代理身份认证
HTTP_PROXY_AUTH -> {
val selectedProxy = route!!.proxy
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
}
return client.proxyAuthenticator.authenticate(route, userResponse)
}
// Response Code : 401
// 身份认证
HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
// Response Code:308,307,300,301,302,303
// 重定向
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
return buildRedirectRequest(userResponse, method)
}
// Response Code : 408
// 请求超时
HTTP_CLIENT_TIMEOUT -> {
// 408的响应码在实践中很少见,出现这个响应码,我们可以向服务器重复发送这个请求(无需对该
// 请求做任何的修改)
// 若客户端不希望在「连接失败」后重试,那么就返回null
if (!client.retryOnConnectionFailure) {
return null
}
val requestBody = userResponse.request.body
// 如果请求带有body,并且该body仅允许发送一次,那么就返回null
if (requestBody != null && requestBody.isOneShot()) {
return null
}
val priorResponse = userResponse.priorResponse
// 如果存在priorResponse,并且priorResponse的响应码也是HTTP_CLIENT_TIMEOUT,
// 那么也返回null,不再进行重试
if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null
}
// 如果Response当中有 Retry-After 这个Header,那么就返回null;
// 如果Response当中没有 Retry-After 这个Header,就不进入If语句。
if (retryAfter(userResponse, 0) > 0) {
return null
}
return userResponse.request
}
// Response Code : 503
// 表示服务器暂时处于超负载或维护状态,可能会包含 RetryAfter 这个Header来告诉客户端
// 何时能够进行访问
HTTP_UNAVAILABLE -> {
...
if (...) {
return userResponse.request
}
return null
}
// Response Code : 421
HTTP_MISDIRECTED_REQUEST -> {
if (...) {
return null
}
...
return userResponse.request
}
else -> return null
}
}
在该方法中,根据不同的响应码可创建不同的请求,其中「重定向」对应的Request
的创建,是调用了buildRedirectRequest
方法,我们查看该方法。
buildRedirectRequest
RetryAndFollowUpInterceptor::buildRedirectRequest
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 客户端不支持重定向,返回null
if (!client.followRedirects) return null
// 获取响应体中Location字段对应的重定向资源位置
val location = userResponse.header("Location") ?: return null
// Don't follow redirects to unsupported protocols.
val url = userResponse.request.url.resolve(location) ?: return null
// If configured, don't follow redirects between SSL and non-SSL.
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null
// 大多数重定向不包含Request Body
val requestBuilder = userResponse.request.newBuilder()
if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code
val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == HTTP_PERM_REDIRECT ||
responseCode == HTTP_TEMP_REDIRECT
if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
requestBuilder.method("GET", null)
} else {
val requestBody = if (maintainBody) userResponse.request.body else 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 (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
// 返回重定向请求
return requestBuilder.url(url).build()
}
小结
下面对RetryAndFollowUpInterceptor
的内部实现进行总结
-
调用拦截器链的
proceed
方法,获取下一个拦截器返回的Response
-
若下面的拦截器在处理请求的过程中抛出「路由连接失败」、「服务器通信失败」的异常,就检测是否可以从失败中恢复,如果可以,就进行请求重试,如果不可以,就抛出异常
-
在本次的
Response
中设置上一次请求的Response
,且上一次请求的Response
的body
为空 -
根据本次响应的
Response Code
,查看是否有需要「重试」或「重定向」的Request
-
若第4步的
Request
不存在,那么就停止计时,并且返回Response
;若第4步的Request
存在,并且带有Request Body
,用户还要求该Request Body
仅能传输一次,那么也是直接返回Response
;否则往下走 -
判断「重试」、「重定向」的次数是否超过最大值,若超过最大值,那么就抛出异常,否则往下走
-
记录「重试」、「重定向」对应的
Request
,以及本次请求返回的Response
-
进行下一次请求