上一篇我们详解了 OkHttp 的众多配置,本篇来看 OkHttp 是如何通过责任链上的内置拦截器完成 HTTP 请求与响应的,目的是更好地深入理解 HTTP 协议。这仍然是一篇偏向于协议实现向的文章,重点在于 HTTP 协议的实现方法与细节,关于责任链模式这些设计模式相关的内容,由于与 HTTP 协议关联不大,因此只是有所提及但不会着重讲解。
1、责任链与内置拦截器
不论是同步还是异步执行网络请求,任务的执行最终都会落到 RealCall 的 getResponseWithInterceptorChain() 中:
@Throws(IOException::class)
internal fun getResponseWithInterceptorChain(): Response {
// 1.构建完整的拦截器堆栈,按添加顺序形成链式处理流程
val interceptors = mutableListOf<Interceptor>()
// 1.1 先添加 OkHttpClient 配置的 interceptors
interceptors += client.interceptors
// 1.2 再添加 OkHttp 内置的拦截器
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
// forWebSocket 用于区分是普通 HTTP 请求还是 WebSocket 握手请求。如果是普通
// HTTP 请求,这里要插入 OkHttpClient 配置的网络拦截器列表 networkInterceptors
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
// 2.创建责任链的初始对象
val chain = RealInterceptorChain(
// RealCall
call = this,
// 拦截器列表
interceptors = interceptors,
// 索引,表示当前处理的是 interceptors 中的哪一个拦截器,责任链的开始传 0
index = 0,
exchange = null,
// 请求,责任链的开始传原始请求
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
// 3.开启责任链的执行
var calledNoMoreExchanges = false
try {
// proceed() 触发责任链
val response = chain.proceed(originalRequest)
if (isCanceled()) {
response.closeQuietly()
throw IOException("Canceled")
}
return response
} catch (e: IOException) {
calledNoMoreExchanges = true
throw noMoreExchanges(e) as Throwable
} finally {
if (!calledNoMoreExchanges) {
noMoreExchanges(null)
}
}
}
由于各个拦截器我们马上就要逐个详解,因此这里就简单聊聊责任链的构成与执行。
被实例化的责任链对象 RealInterceptorChain 实现了 Interceptor.Chain 接口,该接口最重要的方法就是用于执行责任链的 proceed():
class RealInterceptorChain(
// 实际的网络请求对象
internal val call: RealCall,
// 拦截器列表
private val interceptors: List<Interceptor>,
// 索引,表示当前处理的是 interceptors 中的哪一个拦截器,责任链的开始传 0
private val index: Int,
// 负责传输单个 HTTP 请求与响应对。在 [ExchangeCodec](处理实际 I/O 操作)的基础上,
// 实现了连接管理和事件通知的分层逻辑。
internal val exchange: Exchange?,
// 请求
internal val request: Request,
// 连接超时时间
internal val connectTimeoutMillis: Int,
// 读超时
internal val readTimeoutMillis: Int,
// 写超时
internal val writeTimeoutMillis: Int
) : Interceptor.Chain {
@Throws(IOException::class)
override fun proceed(request: Request): Response {
// 生成下一个责任链节点对象,参数 index 告知下一个责任链节点应该取出
// interceptors[index + 1] 这个拦截器进行处理
val next = copy(index = index + 1, request = request)
// 取构造函传入的 index 对应的拦截器
val interceptor = interceptors[index]
// 执行拦截器的处理逻辑并得到响应结果 response
val response = interceptor.intercept(next) ?: throw NullPointerException(
"interceptor $interceptor returned null")
return response
}
}
精简后的 proceed() 代码就是两点:
- 生成责任链的下一个节点,在拦截器执行拦截逻辑的 intercept() 时作为参数传入
- 执行拦截器的拦截逻辑 intercept() 得到响应结果
拦截器的拦截逻辑 intercept() 大致可以分为三部分:
override fun intercept(chain: Interceptor.Chain): Response {
// 1.获取 Request 并根据自身功能对 Request 做出修改...
Request request = chain.request();
...
// 2.执行参数传入的责任链上下一个责任链节点对象 chain 的 proceed(),交接接力棒到下一个节点
val networkResponse = chain.proceed(requestBuilder.build())
// 3.对第 2 步得到的响应 networkResponse 根据自身功能做出修改...
return networkResponse
}
由于每个拦截器的功能不同,因此 1、3 两步是否存在要看具体的拦截器功能。比如重试与重定向拦截器 RetryAndFollowUpInterceptor 就没有第 1 步修改 Request 的需求。但所有拦截器一定都有第 2 步接力棒交接的动作,如果没有,责任链就断了,后面的拦截器就无法工作。
通过 RealInterceptorChain 的构造函数能看到,虽然它保存了所有拦截器的列表 interceptors: List<Interceptor>
,但是在进行处理时,每个 RealInterceptorChain 都是根据 index 取出 interceptors 中的一个拦截器进行拦截操作的,而拦截器的拦截逻辑又会通过 proceed() 执行下一个责任链节点。因此可以将这种责任链模式看成如下结构:
因此,先添加到拦截器列表中的拦截器,就越先获取并修改 Request,但越靠后拿到响应的 Response。
接下来我们会按照 getResponseWithInterceptorChain() 内向 interceptors 列表添加拦截器的顺序逐一介绍这些拦截器。
2、重试与重定向拦截器
RetryAndFollowUpInterceptor 顾名思义就是要做重试与重定向的:
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
// 通过参数传入的 chain 获取 HTTP 请求 Request 请求以及请求任务 RealCall
val realChain = chain as RealInterceptorChain
var request = chain.request
val call = realChain.call
// 重定向次数
var followUpCount = 0
// 上一次请求的响应,用于在重定向返回结果时,把上一次请求的响应体放到本次
// 也就是重定向的响应体 Response 中
var priorResponse: Response? = null
// 是否是新的 ExchangeFinder,当不是重试且允许新路由时为 true
var newExchangeFinder = true
// 可以重试的错误列表
var recoveredFailures = listOf<IOException>()
// 在满足重试或重定向的条件时会一直循环
while (true) {
// 1.准备工作
call.enterNetworkInterceptorExchange(request, newExchangeFinder)
var response: Response
var closeActiveExchange = true
try {
...
try {
// 2.中置工作:责任链交接棒
response = realChain.proceed(request)
newExchangeFinder = true
} catch (e: RouteException) { // 3.请求出错了,进行重试判断
// 3.1 路由异常,连接失败,请求还没有发出去,用 recover() 判断是否满足重试条件
if (!recover(e.lastConnectException, call, request, requestSendStarted = false)) {
throw e.firstConnectException.withSuppressed(recoveredFailures)
} else {
// if 未命中说明满足重试条件,那么就把这个异常放到 recoveredFailures 里面
recoveredFailures += e.firstConnectException
}
// 由于满足重试条件要进行重试了,因此要把 newExchangeFinder 置为 false 了
newExchangeFinder = false
// 由于本次循环失败但满足重试条件,因此跳出本次 while 循环,重试进入下一次循环
continue
} catch (e: IOException) {
// 3.2 请求可能已经发出,但与服务器通信失败,还是用 recover() 判断能否重试
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newExchangeFinder = false
continue
}
// 4.代码能执行到这里说明前面没出错,不用进行重试,那么就开始做后置工作,
// 根据返回的 Response 响应判断是否需要做重定向工作了
// 4.1 如果之前做过重定向,那么 priorResponse 就不为空,将其放到本次响应体中
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build()
}
// 4.2 检查是否需要进行重定向,如果 followUp 为空则不需要
val exchange = call.interceptorScopedExchange
val followUp = followUpRequest(response, exchange)
if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}
// 4.3 需要重定向的情况
val followUpBody = followUp.body
// 如果请求的 Request 有请求体,并且请求体中配置了只允许传输一次,那就不做重定向直接返回
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}
response.body?.closeQuietly()
// 重定向次数超过 MAX_FOLLOW_UPS(默认 20),也不做重定向
if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
// 将重定向请求赋值给 request 作为下一次循环的请求,从而实现重定向请求
// response 赋值给 priorResponse 作为下一次请求的前置请求
request = followUp
priorResponse = response
} finally {
// 5、与 1 的准备工作相对应,现在本次请求结束了,要做收尾工作
call.exitNetworkInterceptorExchange(closeActiveExchange)
}
}
}
RetryAndFollowUpInterceptor 的 intercept() 如果细致划分的话,可以像注释中标记的那样分成 5 步。
第 1 步准备工作与第 5 步收尾工作可以放在一起看,调用的都是 RealCall 的方法:
/**
* 为可能遍历所有网络拦截器的流程做准备。此操作将尝试找到一个 Exchange 来承载请求。
* 如果请求已被缓存满足,则不需要 Exchange。
*
* @param newExchangeFinder 如果这不是一次重试且允许执行新路由,则为 true。
*/
fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
// 确保当前没有已绑定的 Exchange
check(interceptorScopedExchange == null)
// 如果上一个请求体与响应体未关闭则抛异常,确保前一次请求的资源已释放,防止资源泄漏
synchronized(this) {
check(!responseBodyOpen) {
"cannot make a new request because the previous response is still open: " +
"please call response.close()"
}
check(!requestBodyOpen)
}
// 创建新的路由查找器(如非重试场景)
if (newExchangeFinder) {
this.exchangeFinder = ExchangeFinder(
connectionPool,
createAddress(request.url),
this,
eventListener
)
}
}
/**
* @param closeExchange 是否应关闭当前 Exchange(通常因异常或重试导致不再使用)
*/
internal fun exitNetworkInterceptorExchange(closeExchange: Boolean) {
// 线程安全校验:确保当前调用未被释放
synchronized(this) {
check(expectMoreExchanges) { "released" }
}
// 强制关闭当前 Exchange(如发生异常需要重试)
if (closeExchange) {
exchange?.detachWithViolence()
}
// 清理当前拦截器作用域下的 Exchange
interceptorScopedExchange = null
}
第 2 步执行下一个责任链,前面已经分析过。比较重要的是第 3 步重试与第 4 步重定向,我们详细介绍一下。
2.1 重试
从注释的 3.1 与 3.2 两步可以看出,允许进行重试判断的情况有两种:
- 抛出 RouteException,路由异常,连接失败,请求未能发出
- 抛出 IOException,请求可能已经发出,但与服务器通信失败
只有在这两种情况下才能使用 recover() 做进一步的细分判断是否能重试:
/**
* 报告并尝试从与服务器通信的失败中恢复。若异常 `e` 可恢复则返回 true,否则返回 false。
* 注意:带请求体的请求仅在以下情况可恢复:
* 1. 请求体已被缓冲;
* 2. 失败发生在请求发送前。
*/
private fun recover(
e: IOException,
call: RealCall,
userRequest: Request,
requestSendStarted: Boolean
): Boolean {
// 情况 1:应用层禁止重试(通过 OkHttpClient 配置)
if (!client.retryOnConnectionFailure) return false
// 情况 2:请求已发送且为一次性请求体(无法重试发送 Body)
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// 情况 3:异常不可恢复(如 SSL 证书错误)
if (!isRecoverable(e, requestSendStarted)) return false
// 情况 4:无更多可用路由(如所有 IP 尝试失败)
if (!call.retryAfterFailure()) return false
// 所有条件通过,允许恢复并重试
return true
}
下面看每种情况的具体内容。
客户端是否允许重试
取决于 OkHttpClient 的 retryOnConnectionFailure 属性,默认值为 true,可以通过 Builder 模式配置该属性决定是否允许由该 OkHttpClient 发送的所有 Request 进行重试:
@get:JvmName("retryOnConnectionFailure") val retryOnConnectionFailure: Boolean =
builder.retryOnConnectionFailure
单个 Request 是否允许重试
对于已经发送的一次性请求体是无法重试的,它取决于 requestSendStarted 与 requestIsOneShot() 两部分。
对于 requestSendStarted,我们看注释 3.1 与 3.2 传的是不一样的:
- 当抛出 RouteException 时,这时由于路由问题并没有连接到服务器,可以断定请求一定没有被发送,所以直接给 requestSendStarted 传了 false
- 当抛出 IOException 时,只有在这个异常的具体类型是 ConnectionShutdownException 时才认为由于连接中断没有发出请求,其他的情况都认为请求已发出。由于只有 HTTP2 才会抛出这个异常,所以对于 HTTP1 而言,requestSendStarted 一定是 true
再看 requestIsOneShot():
private fun requestIsOneShot(e: IOException, userRequest: Request): Boolean {
val requestBody = userRequest.body
return (requestBody != null && requestBody.isOneShot()) ||
e is FileNotFoundException
}
两个判断条件:
- 如果有请求体,且是一次性请求体
- 发生 FileNotFoundException,即请求体依赖文件,但该文件不存在
第一个条件,RequestBody 的 isOneShot() 是一个默认值为 false 的 open 方法,也就是说默认情况下,请求不是一次性的。但如果需要配置为 OneShot 的话,可以通过重写该函数实现。
第二个条件,假设一个 HTTP 请求的 Body 是基于本地文件的(比如上传文件):
val file = File("image.jpg")
val request = Request.Builder()
.url("https://api.example.com/upload")
.post(file.asRequestBody("image/jpeg".toMediaType()))
.build()
如果因为文件不存在导致 FileNotFoundException,重试也还是相同的结果,因此这种情况也不能重试。
是否为可重试的异常类型
某些异常发生时,是不允许重试的,因为这些异常是真的因为客户端或服务端的代码有问题,即便重试无数次得到的也都是错误结果;而有些异常可能是由于网络波动导致异常发生,换个路线重试可能就会解决问题。因此需要 isRecoverable() 判断,哪些异常可以重试,哪些不可以:
private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
// 1.协议异常,不重试
if (e is ProtocolException) {
return false
}
// 2.一般的 IO 中断异常不会重试,但是如果是 Socket 超时异常,有可能是网络波动造成的,
// 可以尝试其它路线(如果有)
if (e is InterruptedIOException) {
return e is SocketTimeoutException && !requestSendStarted
}
// 3.SSL 握手异常,如果是证书出现问题,不能重试
if (e is SSLHandshakeException) {
// 由 X509TrustManager 抛出的 CertificateException,不重试
if (e.cause is CertificateException) {
return false
}
}
// 4.SSL 未授权异常(证书校验失败,不匹配或过期等问题)不重试
if (e is SSLPeerUnverifiedException) {
// 这里框架作者举的例子是 CertificatePinning 的错误,我们在上一篇详细讲过
// e.g. a certificate pinning error.
return false
}
return true
}
四个判断条件中,后两条是证书验证相关的,这部分在上一篇讲 OkHttpClient 的配置时有讲过,比如 CertificatePinning 的使用、X509 证书验证等等,所以不再赘述。
第 2 条也比较好理解,如果是网络波动造成 Socket 连接超时,重试时可能会躲过网络波动,这时允许重试是合理的。
需要稍作解释的是第 1 条 —— 协议异常。
ProtocolException 是一种 IOException,比如 OkHttp 的 CallServerInterceptor 在其处理责任链的 intercept() 内就抛出了这种异常:
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
try {
...
var code = response.code
...
if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
throw ProtocolException(
"HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
}
return response
}
}
看条件,是状态码为 204 或 205 且响应体内容长度大于 0 时会抛出 ProtocolException。两个状态码的含义分别为:
- 204:No Content,表示无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
- 205:Reset Content,表示重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
也就是说,状态码告诉我们服务器没有返回内容,即没有响应体,但是我们通过代码拿到响应体的内容长度大于 0,这两个条件前后矛盾,有可能是服务器响应本身就存在问题,就算重试也还是得到一个错误结果,于是抛出 ProtocolException 不进行重试。
是否有其他可用路由
我们在配置 OkHttpClient 的时候可能会为客户端配置多个代理,而服务器端的同一个域名也可能会有多个 IP 地址,当某条路线通信失败后,如果存在更多路线,就会换个路线尝试。比如说,restapi.amap.com 解析为 IP1 和 IP2,如果 IP1 通信失败会重试 IP2。
最后我们用一张图来总结一下重试的流程:
2.2 重定向
在进行重定向判断时,框架是将响应 Response 与 Exchange 对象传入 followUpRequest() 得到了一个新的 Request 请求 followUp。在 followUp 不为空且其 body 不为空、followUp 不是一次性 Request 以及重定向次数未超过阈值的情况下,才会进行重定向。那么 followUpRequest() 就是一个很关键的方法:
/**
* 根据接收到的 [userResponse] 确定要进行的 HTTP 请求。这将添加身份验证头、跟随重定向或处理
* 客户端请求超时。如果后续请求不必要或不适用,则返回 null。
*/
@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) {
// 407 代理认证:客户端使用了 HTTP 代理,需要在请求头中添加【Proxy-Authorization】,
// 让代理服务器授权
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")
}
// 调用 OkHttpClient 代理的鉴权接口
return client.proxyAuthenticator.authenticate(route, userResponse)
}
// 401 未授权:有些服务器接口需要验证使用者身份,要在请求头中添加【Authorization】
HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
// HTTP_PERM_REDIRECT:308 永久重定向、HTTP_TEMP_REDIRECT:307 临时重定向、
// HTTP_MULT_CHOICE:300 多种选择、HTTP_MOVED_PERM:301 永久移动、
// HTTP_MOVED_TEMP:302 临时移动、HTTP_SEE_OTHER:303:查看其它地址,这些情况构建重定向请求
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
return buildRedirectRequest(userResponse, method)
}
// 408:服务器等待客户端发送的请求时间过长,超时。实际中用的很少,像 HAProxy 这种服务器
// 会用这个状态码,不需要修改请求,只需再申请一次即可。
HTTP_CLIENT_TIMEOUT -> {
// 假如应用层(指 OkHttpClient)配置了不允许重试就返回 null
if (!client.retryOnConnectionFailure) {
return null
}
// 如果是一次性 RequestBody,不重试
val requestBody = userResponse.request.body
if (requestBody != null && requestBody.isOneShot()) {
return null
}
// 如果上一次响应的状态码就是 408,不重试
val priorResponse = userResponse.priorResponse
if (priorResponse != null && 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
}
// 503:服务不可用。由于超载或系统维护,服务器暂时的无法处理客户端的请求。
// 延时的长度可包含在服务器的 Retry-After 头信息中
HTTP_UNAVAILABLE -> {
val priorResponse = userResponse.priorResponse
// 如果上一次响应的状态码也是 503 则不重试
if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
return null
}
// 如果服务器返回的 Retry-After 是 0,也就是立即重试的意思,框架才重新请求
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
return userResponse.request
}
return null
}
// 421:错误连接。HTTP/2 允许不同域名的请求共享连接(看 RealConnection.isEligible()),
// 但若服务器返回 421,需断开合并连接。
HTTP_MISDIRECTED_REQUEST -> {
val requestBody = userResponse.request.body
// 一次性请求体不重试
if (requestBody != null && requestBody.isOneShot()) {
return null
}
// 无关联的 Exchange 对象(表示无实际网络交互)或者当前连接未启用合并机制,不重试
if (exchange == null || !exchange.isCoalescedConnection) {
return null
}
// 标记连接禁止合并,禁止后续请求复用该连接进行合并(将 noCoalescedConnections 置位)
exchange.connection.noCoalescedConnections()
// 返回原请求重试
return userResponse.request
}
else -> return null
}
}
可以看出,followUpRequest() 会根据不同的响应状态码做出判断是否进行重定向,如需要进行重定向就返回 Request 否则返回 null。
下面我们按照状态码分类介绍上述状态码的处理过程。
3xx
followUpRequest() 将所有 3xx 状态码都放在一个 case 中处理了,就是用 buildRedirectRequest() 创建一个重定向请求:
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 如果 OkHttpClient 配置了不允许重定向则返回 null
if (!client.followRedirects) return null
// 检查响应头中是否有 Location 字段,该字段的值能否解析成 URL,
// 可以则解析并保存 URL,否则返回 null 不进行重定向
val location = userResponse.header("Location") ?: return null
val url = userResponse.request.url.resolve(location) ?: return null
// 检查重需要定向的 URL 与此前请求的 URL 的 scheme 是否相同。scheme 在 OkHttp 这个
// 框架中就只有 http 或 https 两个值。假如 OkHttpClient 配置 followSslRedirects 为
// false,那就不允许在 SSL(HTTPS)与非 SSL(HTTP)协议之间重定向
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null
// 大多数重定向请求都没有请求体
val requestBuilder = userResponse.request.newBuilder()
// 不是 GET 或 HEAD 方法的话,是允许有请求体的
if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code
// 如果请求方法是 PROPFIND 或者响应状态码为 308、307,需要保持请求体
val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == HTTP_PERM_REDIRECT ||
responseCode == HTTP_TEMP_REDIRECT
// 如果请求方法不是 PROPFIND 且响应状态码不是 308、307,重定向方法要使用 GET
if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) {
requestBuilder.method("GET", null)
} else {
// 如果请求方法是 PROPFIND 或者响应状态码是 307、308 之一,则使用原请求方法与请求体
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")
}
}
// 当重定向到不同主机时,移除 Authorization 头
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
return requestBuilder.url(url).build()
}
buildRedirectRequest() 可以大致分为两部分:
- 检查是否满足重定向条件,不满足则返回 null,涉及到的不满足情况包括:
- OkHttpClient 的重定向配置 followRedirects 设置为 false
- 返回的响应体是 3xx 要求重定向,但响应头中没有 Location 或者 Location 的值无法解析为 URL
- 原请求的 URL 与原请求对应的响应头中 Location 指定的 URL 发生了 SSL 协议切换(对于 OkHttp 框架而言,就是一个 URL 是 HTTP,另一个 URL 是 HTTPS),但 OkHttpClient 又关闭了跨 SSL 重定向的配置 followSslRedirects
- 满足重定向条件,就构造重定向的请求头与请求体:
- 对于没有请求体的 GET 与 HEAD 方法而言,只需要额外再判断一个跨主机重定向时去掉 Authorization 请求头即可
- 对于其他可以有请求体的方法,先判断是否保持原请求的请求体 maintainBody,对于明确需要保持的方法 PROPFIND(目前 redirectsWithBody() 只有传入 PROPFIND 才返回 true)或者状态码为 307、308 时,maintainBody 为 true
- 随后决定重定向请求使用哪个方法以及请求体。如果原请求方法不是 PROPFIND(目前 redirectsToGet() 除了 PROPFIND 以外其余都是 true),且状态码不是 307 与 308,那么重定向请求的方法就是 GET 且没有方法体;否则重定向方法就是 PROPFIND 且请求体为原请求的请求体
为什么 307、308 以及 PROPFIND 需要做出特殊的判断与处理,强制保留原请求的方法和请求体?
其中 307、308 是 HTTP 重定向规范(RFC 7231 (HTTP/1.1) 和 RFC 7538 (HTTP/308))中规定的,要求客户端在重定向时保持原始请求方法和请求体。
至于 PROPFIND 是 WebDAV 协议的扩展方法,行为类似于标准 HTTP 的 GET,但需要携带 XML 请求体以描述查询条件。这个方法本身就需要保持请求体,因为服务器可能依赖请求体中的 XML 内容处理查询。
因此关于重定向的处理思路可以总结为如下表格:
条件 | 操作 | 示例场景 |
---|---|---|
状态码为307/308 | 强制保留方法和请求体 | 服务器要求保持原始请求 |
方法为PROPFIND/POST/PUT | 保留请求体(需配合307/308) | WebDAV查询或表单提交 |
其他状态码(301/302) | 改为GET并移除请求体 | 传统重定向逻辑 |
4xx
我们先说两个比较相近的:HTTP_UNAUTHORIZED(401 未授权)与 HTTP_PROXY_AUTH(407 代理认证)。
实际上这两个状态码我们在上一篇讲 OkHttp 配置时已经讲过:当你要访问的服务器资源需要授权才能访问,而你在请求中又没有携带授权信息相关的请求头时,服务器会返回 401 状态码;407 是类似的,当你使用代理服务器又没有提供鉴权请求头时服务器就会返回 407。
清楚原因后,对于两种状态码的处理方案也就呼之欲出了,就是在请求时带上授权信息呗。按照 OkHttp 框架给出的,对于 401 调用 OkHttpClient 的 authenticator 的 authenticate() 进行授权,对于 407 则调用 OkHttpClient 的 proxyAuthenticator 的 authenticate() 授权。但 authenticator 与 proxyAuthenticator 的默认值都是 Authenticator 接口的默认实现 Authenticator.NONE:
fun interface Authenticator {
@Throws(IOException::class)
fun authenticate(route: Route?, response: Response): Request?
companion object {
/** An authenticator that knows no credentials and makes no attempt to authenticate. */
@JvmField
val NONE: Authenticator = AuthenticatorNone()
private class AuthenticatorNone : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? = null
}
}
}
因此通过这种方式需要自己实现 Authenticator 接口:
val client = OkHttpClient.Builder()
.authenticator(object : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// 添加 Authorization 与 Proxy-Authorization 请求头
return response.request.newBuilder()
.header("Authorization", "Bearer xxxxx....")
.header("Proxy-Authorization", "xxxxx....")
.build()
}
})
然后说一下 HTTP_CLIENT_TIMEOUT(408),是服务器等待客户端发送超时。这个实际上跟重定向没啥关系,属于超时重试的逻辑,在刨除不满足重试的条件后,无需修改请求进行重试即可。我们来看看它这种情况下用到的 retryAfter():
private fun retryAfter(userResponse: Response, defaultDelay: Int): Int {
val header = userResponse.header("Retry-After") ?: return defaultDelay
if (header.matches("\\d+".toRegex())) {
return Integer.valueOf(header)
}
return Integer.MAX_VALUE
}
根据响应头中的 Retry-After 字段,返回当前距离下一次重试需要的时间。当前会忽略 HTTP 的日期,并且假设任何 int 型的非 0 值都是延迟。
实际上就是返回一个延迟的值,表示过多长时间之后再重试。
5xx
在 followUpRequest() 中只有 HTTP_UNAVAILABLE(503 服务不可用)一个 5 开头的状态码,它是指由于超载或系统维护,服务器暂时的无法处理客户端的请求。需要在上一次响应不是 HTTP_UNAVAILABLE 的情况下通过 retryAfter() 判断是否可以立即进行重试,如果服务器返回的 Retry-After 响应头的值不为 0,那么就放弃重试。