B15_HTTP解析与OkHttp拦截器

Android 基础补强 B15|HTTP、JSON 与 OkHttp:一次请求究竟在哪一层失败

发布摘要:从原始 HTTP 响应拆解状态码、业务码与解析错误,再通过 OkHttp 拦截器理解请求链和响应体释放。标签:HTTP、OkHttp、JSON、Android 网络。

接口返回了 200,页面为什么仍然没有文章?可能响应中的业务码表示失败,也可能 JSON 结构不符合客户端预期,还可能模型转换后被过滤掉。只在最外层显示"网络异常",会把服务器状态、数据协议和客户端逻辑混成一个问题。本篇暂时绕开高层封装,看一次请求如何变成可展示的数据。

对应《第一行代码》第 11 章 HTTP、解析与网络库内容,是 D09 真实接口课的基础补强。示例使用自定义受控响应格式,不代表玩 Android 接口字段。代码尚未在用户工程编译运行,需要 OkHttp、Android 自带 org.json 和网络权限。

一、先区分三层成功

传输成功表示拿到了响应,并不要求状态码是成功;HTTP 成功通常指 2xx,但具体接口还定义自己的业务码。业务成功以后,客户端仍需确认必要字段可解析,并能转换为领域模型。

例如受控接口返回 {"code":0,"data":{"id":7,"title":"网络实验"}}。如果 HTTP 为 500,先归类为 HTTP 错误;HTTP 为 200、code 为非零,属于业务错误;code 为零但 data 缺失,属于协议或解析问题。空文章列表则可能是合法业务结果,不应该被硬塞进异常。

第一步在 Fake 或本地测试服务准备四种响应;第二步写原始读取函数;第三步加入解析;第四步再接入 Repository 做异常分类。生产接口字段必须以它自己的文档为准,不能为了复用示例偷偷改字段名。

二、读取响应体时确保关闭

下面函数明确是阻塞 API,只能由后台线程或专用 IO 执行上下文调用。它返回受控 ArticleBrief,错误分为 HTTP、业务和协议。导入完整列出;不要在 Compose 函数体或主线程点击回调直接调用。

kotlin 复制代码
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException

data class ArticleBrief(val id: Long, val title: String)
class HttpFailure(val status: Int) : IOException("HTTP $status")
class BusinessFailure(val code: Int) : Exception("business $code")
class ProtocolFailure(cause: Throwable) : Exception("invalid payload", cause)

fun fetchArticleBlocking(client: OkHttpClient, url: String): ArticleBrief {
    val request = Request.Builder().url(url).get().build()
    return client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) throw HttpFailure(response.code)
        val text = response.body?.string() ?: throw IOException("empty body")
        try {
            val root = JSONObject(text)
            val code = root.getInt("code")
            if (code != 0) throw BusinessFailure(code)
            val data = root.getJSONObject("data")
            val id = data.getLong("id")
            if (data.isNull("title")) throw JSONException("title is null")
            val title = data.getString("title").trim()
            if (title.isEmpty()) throw JSONException("title is empty")
            ArticleBrief(id, title)
        } catch (error: JSONException) {
            throw ProtocolFailure(error)
        }
    }
}

ResponseBody 是一次消费的流式资源,string 会读取其内容,读取后不能再次当作未消费的正文。use 让退出路径关闭响应,包括 HTTP 错误和解析异常。OkHttp ResponseBody 官方源码说明

这是便于看清层次的小响应教学实现,不适合无上限下载大文件;大内容应流式处理并限制大小。JSON 原始解析也不是强类型校验的终点,真正模型约束可在转换层进一步检查。

三、拦截器处理横切信息,不替代业务层

拦截器适合统一请求头、诊断耗时和请求编号。下面实现只添加受控应用标识,不读取或修改响应正文,避免日志先消费正文导致后续解析失败。导入为 okhttp3.Interceptor 和 okhttp3.Response。

kotlin 复制代码
class ClientHeaderInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header("X-Client", "DevCommunity-Lab")
            .build()
        return chain.proceed(request)
    }
}

val client = OkHttpClient.Builder()
    .addInterceptor(ClientHeaderInterceptor())
    .build()

应用拦截器观察应用层的一次调用;网络拦截器能观察实际网络交换中的重定向等中间过程,命中无需联网的缓存时也可能不执行。二者日志数量不同不代表用户点了两次。OkHttp 官方拦截器文档

拦截器按添加顺序形成调用链,请求向内走,响应向外返回。如果自行重试并再次 proceed,前一个响应体必须先关闭,还要判断请求是否适合重试。支付提交或新增记录之类操作不能因为"网络抖动"就随意重复,本练习只讨论幂等读取。

鉴权头只应发往预期服务,日志不能输出完整令牌或敏感参数。统一添加请求头也要考虑共享客户端访问其他主机的边界,不能把所有请求都默认当成自己的服务器。

四、切到 IO 不等于完成取消适配

把阻塞 execute 包进 withContext(IO) 可以避免阻塞主线程,但协程取消不必然立即取消底层 Call。若要完善取消,应使用成熟的挂起适配,例如 Retrofit 的挂起接口,或自己把取消事件连接到 Call.cancel,并处理回调与取消竞态。

客户端对象也应复用,避免每次请求都创建一套连接池和线程资源。一次响应读取完毕后,数据才交给 Repository 的转换和存储步骤;UI 看到的是明确状态,而不是 OkHttp Response 对象。

超时也有不同范围:连接、读取、写入和整个调用时间限制不相同。遇到慢请求先记录在哪个阶段停住,不能只把所有超时调大。对真实公共服务做故障实验应使用本地替代或 MockWebServer,避免反复制造异常请求影响对方服务。

五、故障实验与预期

依次返回 HTTP 500、HTTP 200 加非零业务码、截断 JSON、合法响应。预期分类分别落到 HTTP、业务、协议和成功。再返回合法空集合时,需要使用该接口对应的列表模型,证明空结果不是网络失败。

在调试拦截器中故意先读取 body.string 再把原响应原样返回,观察解析失败;移除提前消费后重测。该实验只针对受控测试响应,不打印真实私密正文。

最后取消一个被测试服务延迟的调用,记录底层是否收到 cancel;如果只是结束 UI 等待而请求仍继续,就如实说明取消尚未打通,不能宣称"协程天然终止所有网络操作"。

六、原创面试问答与追问

本篇 HTTP 与拦截器问题属于书籍和课程自拟延伸,可联系题库线程与进程理解执行上下文。

问:HTTP 200 为什么仍可能是失败? 答:传输协议与业务协议有不同成功条件,客户端还需要解析和模型校验。追问:解析失败要显示断网吗?不应该,应保留真实分类便于定位。

问:为什么 response body 不能随意读两次? 答:它是可消费的资源流,不是自动保存的无限副本。追问:日志怎么办?使用受控、脱敏、限量的诊断方式,避免破坏业务读取。

问:应用拦截器和网络拦截器为什么计数不同? 答:它们观察的层级不同,缓存和重定向会改变实际网络交换次数。追问:哪个适合统计用户操作?两者都不能直接替代用户事件统计。

七、练习与验收

建议在 D09 后安排九十分钟基础实验:先画请求、HTTP 响应、业务解析和模型转换四层,再给每层制造一次失败。额外三十分钟检查响应释放和取消语义。

交付四种响应样本、异常分类结果、一段不读取正文的拦截器,以及阻塞调用的线程说明。能从错误现象定位所在层次,再回到 Retrofit 封装,才不会把所有问题都归咎于网络库。

相关推荐
梦帮科技1 小时前
【3.0修订版】音乐权利 NFT:MusicRightsNFT 的确权思想与授权关联
网络协议·算法·web3·区块链·密码学·智能合约·零知识证明
acd120091 小时前
RPC接口超时配置明明没问题,线上却随机超时
网络·网络协议·rpc
砚凝霜1 小时前
【软考信息安全】第九章 虚拟专用网与IPSec/SSL安全协议技术原理
网络·网络协议·ssl
巨擎网络17 小时前
为什么打开 App,消息才一股脑涌进来?
网络协议·app
福兮说21 小时前
IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出
javascript·网络·网络协议·tcp/ip·mysql·golang
0+1111 天前
Linux --应用层协议HTTP
网络·网络协议·http
网络小江1 天前
组网第十课:编码、速率与排障——从比特到信号的最后一公里
网络协议
网络小江1 天前
上网第四十三课:路由原理——数据包是怎么找到路的
网络协议
To_OC1 天前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http