Android 网络全栈攻略(四)—— 从 OkHttp 拦截器来看 HTTP 协议一

上一篇我们详解了 OkHttp 的众多配置,本篇来看 OkHttp 是如何通过责任链上的内置拦截器完成 HTTP 请求与响应的,目的是更好地深入理解 HTTP 协议。这仍然是一篇偏向于协议实现向的文章,重点在于 HTTP 协议的实现方法与细节,关于责任链模式这些设计模式相关的内容,由于与 HTTP 协议关联不大,因此只是有所提及但不会着重讲解。

1、责任链与内置拦截器

不论是同步还是异步执行网络请求,任务的执行最终都会落到 RealCall 的 getResponseWithInterceptorChain() 中:

  @Throws(IOException::class)internal fun getResponseWithInterceptorChain(): Response {// 1.构建完整的拦截器堆栈,按添加顺序形成链式处理流程val interceptors = mutableListOf<Interceptor>()// 1.1 先添加 OkHttpClient 配置的 interceptorsinterceptors += client.interceptors// 1.2 再添加 OkHttp 内置的拦截器interceptors += RetryAndFollowUpInterceptor(client)interceptors += BridgeInterceptor(client.cookieJar)interceptors += CacheInterceptor(client.cache)interceptors += ConnectInterceptor// forWebSocket 用于区分是普通 HTTP 请求还是 WebSocket 握手请求。如果是普通// HTTP 请求,这里要插入 OkHttpClient 配置的网络拦截器列表 networkInterceptorsif (!forWebSocket) {interceptors += client.networkInterceptors}interceptors += CallServerInterceptor(forWebSocket)// 2.创建责任链的初始对象val chain = RealInterceptorChain(// RealCall call = this,// 拦截器列表interceptors = interceptors,// 索引,表示当前处理的是 interceptors 中的哪一个拦截器,责任链的开始传 0index = 0,exchange = null,// 请求,责任链的开始传原始请求request = originalRequest,connectTimeoutMillis = client.connectTimeoutMillis,readTimeoutMillis = client.readTimeoutMillis,writeTimeoutMillis = client.writeTimeoutMillis)// 3.开启责任链的执行var calledNoMoreExchanges = falsetry {// proceed() 触发责任链val response = chain.proceed(originalRequest)if (isCanceled()) {response.closeQuietly()throw IOException("Canceled")}return response} catch (e: IOException) {calledNoMoreExchanges = truethrow 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 中的哪一个拦截器,责任链的开始传 0private 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]// 执行拦截器的处理逻辑并得到响应结果 responseval response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null")return response}
}

精简后的 proceed() 代码就是两点:

  1. 生成责任链的下一个节点,在拦截器执行拦截逻辑的 intercept() 时作为参数传入
  2. 执行拦截器的拦截逻辑 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() 执行下一个责任链节点。因此可以将这种责任链模式看成如下结构:

2024-12-6.记忆责任链

因此,先添加到拦截器列表中的拦截器,就越先获取并修改 Request,但越靠后拿到响应的 Response。

接下来我们会按照 getResponseWithInterceptorChain() 内向 interceptors 列表添加拦截器的顺序逐一介绍这些拦截器。

2、重试与重定向拦截器

RetryAndFollowUpInterceptor 顾名思义就是要做重试与重定向的:

  @Throws(IOException::class)override fun intercept(chain: Interceptor.Chain): Response {// 通过参数传入的 chain 获取 HTTP 请求 Request 请求以及请求任务 RealCallval realChain = chain as RealInterceptorChainvar request = chain.requestval call = realChain.call// 重定向次数var followUpCount = 0// 上一次请求的响应,用于在重定向返回结果时,把上一次请求的响应体放到本次// 也就是重定向的响应体 Response 中var priorResponse: Response? = null// 是否是新的 ExchangeFinder,当不是重试且允许新路由时为 truevar newExchangeFinder = true// 可以重试的错误列表var recoveredFailures = listOf<IOException>()// 在满足重试或重定向的条件时会一直循环while (true) {// 1.准备工作call.enterNetworkInterceptorExchange(request, newExchangeFinder)var response: Responsevar closeActiveExchange = truetry {...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 = falsecontinue}// 4.代码能执行到这里说明前面没出错,不用进行重试,那么就开始做后置工作,// 根据返回的 Response 响应判断是否需要做重定向工作了// 4.1 如果之前做过重定向,那么 priorResponse 就不为空,将其放到本次响应体中if (priorResponse != null) {response = response.newBuilder().priorResponse(priorResponse.newBuilder().body(null).build()).build()}// 4.2 检查是否需要进行重定向,如果 followUp 为空则不需要val exchange = call.interceptorScopedExchangeval followUp = followUpRequest(response, exchange)if (followUp == null) {if (exchange != null && exchange.isDuplex) {call.timeoutEarlyExit()}closeActiveExchange = falsereturn response}// 4.3 需要重定向的情况val followUpBody = followUp.body// 如果请求的 Request 有请求体,并且请求体中配置了只允许传输一次,那就不做重定向直接返回if (followUpBody != null && followUpBody.isOneShot()) {closeActiveExchange = falsereturn 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 = followUppriorResponse = response} finally {// 5、与 1 的准备工作相对应,现在本次请求结束了,要做收尾工作call.exitNetworkInterceptorExchange(closeActiveExchange)}}}

RetryAndFollowUpInterceptor 的 intercept() 如果细致划分的话,可以像注释中标记的那样分成 5 步。

第 1 步准备工作与第 5 步收尾工作可以放在一起看,调用的都是 RealCall 的方法:

  /*** 为可能遍历所有网络拦截器的流程做准备。此操作将尝试找到一个 Exchange 来承载请求。* 如果请求已被缓存满足,则不需要 Exchange。** @param newExchangeFinder 如果这不是一次重试且允许执行新路由,则为 true。*/fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {// 确保当前没有已绑定的 Exchangecheck(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()}// 清理当前拦截器作用域下的 ExchangeinterceptorScopedExchange = null}

第 2 步执行下一个责任链,前面已经分析过。比较重要的是第 3 步重试与第 4 步重定向,我们详细介绍一下。

2.1 重试

从注释的 3.1 与 3.2 两步可以看出,允许进行重试判断的情况有两种:

  1. 抛出 RouteException,路由异常,连接失败,请求未能发出
  2. 抛出 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.bodyreturn (requestBody != null && requestBody.isOneShot()) ||e is FileNotFoundException}

两个判断条件:

  1. 如果有请求体,且是一次性请求体
  2. 发生 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.codeval method = userResponse.request.methodwhen (responseCode) {// 407 代理认证:客户端使用了 HTTP 代理,需要在请求头中添加【Proxy-Authorization】,// 让代理服务器授权HTTP_PROXY_AUTH -> {val selectedProxy = route!!.proxyif (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)配置了不允许重试就返回 nullif (!client.retryOnConnectionFailure) {return null}// 如果是一次性 RequestBody,不重试val requestBody = userResponse.request.bodyif (requestBody != null && requestBody.isOneShot()) {return null}// 如果上一次响应的状态码就是 408,不重试val priorResponse = userResponse.priorResponseif (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 配置了不允许重定向则返回 nullif (!client.followRedirects) return null// 检查响应头中是否有 Location 字段,该字段的值能否解析成 URL,// 可以则解析并保存 URL,否则返回 null 不进行重定向val location = userResponse.header("Location") ?: return nullval 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.schemeif (!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,重定向方法要使用 GETif (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 nullrequestBuilder.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() 可以大致分为两部分:

  1. 检查是否满足重定向条件,不满足则返回 null,涉及到的不满足情况包括:
    • OkHttpClient 的重定向配置 followRedirects 设置为 false
    • 返回的响应体是 3xx 要求重定向,但响应头中没有 Location 或者 Location 的值无法解析为 URL
    • 原请求的 URL 与原请求对应的响应头中 Location 指定的 URL 发生了 SSL 协议切换(对于 OkHttp 框架而言,就是一个 URL 是 HTTP,另一个 URL 是 HTTPS),但 OkHttpClient 又关闭了跨 SSL 重定向的配置 followSslRedirects
  2. 满足重定向条件,就构造重定向的请求头与请求体:
    • 对于没有请求体的 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. */@JvmFieldval 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 defaultDelayif (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,那么就放弃重试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/84143.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

免费AI工具整理

1、NVIDIA models ALL&#xff1a;Try NVIDIA NIM APIs example&#xff1a;llama-3.1-405b-instruct Model by Meta | NVIDIA NIM 2、文心一言 文心一言 3、纳米AI 纳米AI搜索 4、其他 ChatGPT 镜像网址&#xff08;5月持续更新&#xff09; - 最优网址

C++ std::find() 函数全解析

std::find()是C标准库中用于线性查找的基础算法&#xff0c;属于<algorithm>头文件&#xff0c;可应用于任何支持迭代器的容器。 一、函数原型与参数 template< class InputIt, class T > InputIt find( InputIt first, InputIt last, const T& value );​​…

MySQL--day6--单行函数

&#xff08;以下内容全部来自上述课程&#xff09; 单行函数 1. 内置函数及分类 单行函数聚合函数&#xff08;或分组函数&#xff09; 1.1 单行函数特点 操作数据对象接受参数返回一个结果只对一行进行变换每行返回一个结果可以嵌套参数可以是一列或一个值 2. 数值函…

GO语言学习(九)

GO语言学习&#xff08;九&#xff09; 上一期我们了解了实现web的工作中极为重要的net/http抱的细节讲解&#xff0c;大家学会了实现web开发的一些底层基础知识&#xff0c;在这一期我来为大家讲解一下web工作的一个重要方法&#xff0c;&#xff1a;使用数据库&#xff0c;现…

解决MybatisPlus使用Druid1.2.11连接池查询PG数据库报Merge sql error的一种办法

目录 前言 一、问题重现 1、环境说明 2、重现步骤 3、错误信息 二、关于LATERAL 1、Lateral作用场景 2、在四至场景中使用 三、问题解决之道 1、源码追踪 2、关闭sql合并 3、改写处理SQL 四、总结 前言 在博客&#xff1a;【写在创作纪念日】基于SpringBoot和PostG…

嵌入式学习--江协51单片机day8

这个本来应该周末写的&#xff0c;可是一直想偷懒&#xff0c;只能是拖到周一了&#xff0c;今天把51结个尾&#xff0c;明天开始学32了。 学习内容LCD1602&#xff0c;直流电机&#xff0c;AD/DA&#xff0c;红外遥控 LCD1602 内部的框架结构 屏幕小于数据显示区&#xff…

HUAWEI华为MateBook D 14 2021款i5,i7集显非触屏(NBD-WXX9,NbD-WFH9)原装出厂Win10系统

适用型号&#xff1a;NbD-WFH9、NbD-WFE9A、NbD-WDH9B、NbD-WFE9、 链接&#xff1a;https://pan.baidu.com/s/1qTCbaQQa8xqLR-4Ooe3ytg?pwdvr7t 提取码&#xff1a;vr7t 华为原厂WIN系统自带所有驱动、出厂主题壁纸、系统属性联机支持标志、系统属性专属LOGO标志、Office…

【Python】Python 装饰器的用法总结

在 Python 中&#xff0c;装饰器&#xff08;Decorator&#xff09; 是一种设计模式&#xff0c;用于在不修改函数或类代码的情况下动态地扩展其功能。装饰器广泛应用于日志记录、性能监控、权限验证等场景&#xff0c;提供了一种简洁优雅的方式来“包裹”现有的代码。本文将介…

【C++】控制台小游戏

移动&#xff1a;W向上&#xff0c;S上下&#xff0c;A向左&#xff0c;D向右 程序代码&#xff1a; #include <iostream> #include <conio.h> #include <windows.h> using namespace std;bool gameOver; const int width 20; const int height 17; int …

「MATLAB」计算校验和 Checksum

什么是校验和 是一个算法&#xff0c;将一串数据累加&#xff0c;得到一个和。 MATLAB程序 function c_use Checksum(packet) %Checksum 求校验和 % 此处checksum提供详细说明checksum 0;for i 1:length(packet)value hex2dec(packet(i));checksum checksum value; …

JavaScript面试题之消息队列

JavaScript消息队列详解&#xff1a;单线程的异步魔法核心 在JavaScript的单线程世界中&#xff0c;消息队列&#xff08;Message Queue&#xff09;是实现异步编程的核心机制&#xff0c;它像一位高效的调度员&#xff0c;让代码既能“一心多用”又避免卡顿。本文将深入剖析消…

京东外卖分润系统部署实操!0门槛入驻+全平台接入+自定义比例...这些人,赚翻了!

随着京东外卖的发展势头日渐迅猛&#xff0c;许多创业者们的态度也逐渐从原本的观望转变为了切实的行动&#xff0c;并开始通过各个渠道询问起了京东外卖自动分润系统部署相关的各项事宜&#xff0c;连带着以京东外卖自动分润系统质量哪家强为代表的多个问题&#xff0c;也成为…

【办公类-18-06】20250523(Python)“口腔检查涂氟信息”批量生成打印(学号、姓名、学校、班级、身份证、户籍、性别、民族)

背景需求: 6月是常规体检,前几天发了体检表(验血单),用Python做了姓名等信息的批量打印 【办公类-18-04】20250520(Python)“验血单信息”批量生成打印(学校、班级、姓名、性别)-CSDN博客文章浏览阅读969次,点赞19次,收藏11次。【办公类-18-04】20250520(Python)…

Python邮件处理:POP与SMTP

poplib简介 poplib 是Python 3中的官方邮件库&#xff0c;实现了POP的标准&#xff1a;RFC1939&#xff0c;用于邮件的收取。与之类似的还有imaplib 。 &#xff08;注&#xff1a;本文仅拿pop举例&#xff09; poplib的使用方法&#xff0c;就是几步&#xff1a; 先创建一…

IP风险度自检,多维度守护网络安全

如今IP地址不再只是网络连接的标识符&#xff0c;更成为评估安全风险的核心维度。IP风险度通过多维度数据建模&#xff0c;量化IP地址在网络环境中的安全威胁等级&#xff0c;已成为企业反欺诈、内容合规、入侵检测的关键工具。据Gartner报告显示&#xff0c;2025年全球78%的企…

Flink集成资源管理器

Flink集成资源管理器 Apache Flink 支持多种资源管理器&#xff0c;主要包括以下几种‌&#xff1a; YARN ResourceManager ‌&#xff1a;适用于使用 Hadoop YARN 作为资源管理器的环境。YARN ResourceManager 负责管理集群中的资源&#xff0c;包括 CPU、内存等&#xff0c;并…

upload 文件上传审计

目录 LOW Medium HIgh Impossible 概述 很多Web站点都有文件上传的接口&#xff08;比如注册时上传头像等&#xff09;&#xff0c;由于没有对上传的文件类型进行严格限制&#xff0c;导致可以上传一些文件&#xff08;比如Webshell&#xff09;。 上传和SQL、XSS等都是主流…

【freertos-kernel】list

freertos list 基本类型结构体ListItem_t &#xff08;list.h&#xff09;List_t &#xff08;list.h&#xff09; 宏函数函数vListInitialisevListInitialiseItemvListInsertEndvListInsertuxListRemove 基本类型 freertos为了兼容性&#xff0c;重新定义了基本类型&#xff…

游戏盾的功有哪些?

游戏盾的功能主要包括以下几方面&#xff1a; 一、网络攻击防护 DDoS攻击防护&#xff1a; T级防御能力&#xff1a;游戏盾提供分布式云节点防御集群&#xff0c;可跨地区、跨机房动态扩展防御能力和负载容量&#xff0c;轻松达到T级别防御&#xff0c;有效抵御SYN Flood、UD…

PycharmFlask 学习心得:路由(3-4)

对路由的理解&#xff1a; 用户输入网址 例如&#xff1a;http://localhost:5000/hello 浏览器会向这个地址发起一个 HTTP 请求&#xff08;比如 GET 请求&#xff09; 请求到达 Flask 的服务器 Flask 监听着某个端口&#xff08;如 5000&#xff09;&#xff0c;收到请求后…