资讯动态

第八篇:Ktor 异常体系:断网、Timeout、HTTP、JSON 与业务错误如何统一成 AppError

发布时间:2026/8/22 16:39:58 来源:尧图企业网站定制
前面几篇我已经把 Ktor 网络请求的主链路逐渐搭起来了ApiService ↓ NetworkClient ↓ HttpClient ↓ HttpResponse ↓ ApiResponseT ↓ 检查 code ↓ data ↓ T成功流程已经比较清楚。但是正式项目真正麻烦的地方往往不是请求成功怎么办而是请求失败怎么办例如一次请求可能遇到请求前已经明确断网 请求过程中网络突然断开 DNS 解析失败 连接服务器失败 Connect Timeout Request Timeout Socket Timeout HTTP 401 HTTP 404 HTTP 500 JSON 解析失败 业务 code ! 0 未知异常如果这些错误直接一路抛到 ViewModelViewModel ↓ catch Throwable ↓ 判断各种 Ktor / Engine / Serialization 异常业务层很快就会和底层网络框架绑定。所以这一篇要解决的是如何把不同阶段产生的网络错误统一转换成项目自己的AppError。最终希望形成请求前网络检查 │ └── 明确断网 ↓ AppError.Network 真正网络请求 ↓ Throwable ↓ ExceptionMapper ↓ AppError业务层最终只关心Network Timeout Unauthorized Forbidden NotFound Server Parse Business Unknown而不需要了解底层具体异常类型。一、为什么不能直接 catch Exception最简单的时候我们可能会写try { val user networkClient.getUser( path users/1001, ) } catch (e: Exception) { // 请求失败 }功能上当然可以。但问题在于Exception到底代表什么可能是手机已经断网 请求过程中断网 服务器连接失败 Timeout 401 500 JSON 解析失败 code ! 0如果全部统一显示请求失败显然不够。不同错误通常意味着不同日志 不同重试策略 不同业务动作 不同 UI 提示所以需要先把错误分层。二、一次请求其实有多个失败阶段完整一点可以把一次请求理解成业务发起请求 ↓ 请求前检查 ↓ 网络执行 ↓ HTTP Response ↓ Response Body ↓ JSON 解析 ↓ ApiResponseT ↓ 业务 code ↓ data因此失败可能发生在① 请求真正发出之前 ② 网络连接 / 传输阶段 ③ HTTP 协议阶段 ④ JSON 解析阶段 ⑤ 业务协议阶段结构Request │ ├── 请求前发现断网 │ ↓ Network / Engine │ ├── 网络突然断开 ├── DNS / Connect 失败 ├── Timeout ↓ HTTP Response │ ├── 401 ├── 404 ├── 500 ↓ JSON │ ├── 数据格式错误 ↓ ApiResponseT │ ├── code ! 0 ↓ data所以我们首先要建立错误发生在哪一层而不是看到所有问题都叫网络异常三、第一层断网最好先做“请求前快速失败”tips补充篇 8.1《Ktor/KMP 断网处理为什么请求前要先判断网络状态》以前 Android 项目中我们经常会在 OkHttp Interceptor 中做请求准备发送 ↓ 检查当前网络状态 ↓ 没有网络 ↓ 直接抛 NoNetworkException ↓ 不执行 chain.proceed()也就是说如果已经明确知道设备没有互联网连接那么根本没必要继续进入真正的 HTTP 网络请求。在 Ktor/KMP 中也可以保持同样的思想。例如我们可以抽象interface NetworkConnectivityProvider { fun isConnected(): Boolean }然后在统一请求入口class NetworkClient( private val client: HttpClient, private val connectivityProvider: NetworkConnectivityProvider, ) { suspend inline fun reified T get( path: String, ): T { if (!connectivityProvider.isConnected()) { throw NoNetworkException() } return client .get(path) .body() } }执行流程ApiService ↓ NetworkClient ↓ NetworkConnectivityProvider ↓ 有可用网络吗 / \ 否 是 ↓ ↓ 直接失败 HttpClient ↓ Engine ↓ HTTP 请求如果明确没网HttpClient Engine DNS Connect Retry Timeout这些后续流程都不需要进入。因此请求前检查最大的价值不是单纯“捕获断网异常”而是Fail Fast已知请求不可能成功就不要继续做无意义的网络工作。它可以减少无意义的网络栈执行 无意义的连接尝试 不必要的 Timeout 等待 不必要的 Retry 日志噪音同时用户也能更快得到反馈。Android 平台可以通过ConnectivityManager/NetworkCapabilities判断网络状态。需要注意NET_CAPABILITY_INTERNET只表示网络被配置为可访问互联网并不保证当前真的可以访问公网NET_CAPABILITY_VALIDATED表示系统最近成功验证过公网连通性更接近我们需要的“互联网可用”语义。即便已经VALIDATED网络仍可能随后突然失效。所以请求前断网检查是一道很有价值的第一道防线但它不能成为唯一防线。这个问题后面还会继续讲。四、第二层真实网络请求仍然可能失败假设请求前检查NetworkConnectivityProvider ↓ 有网是不是意味着接下来一定成功不是。因为可能10:00:00.000 检查网络 ↓ 有网 10:00:00.020 开始请求 10:00:00.050 Wi-Fi 断开也可能系统认为互联网可用 ↓ 但目标服务器当前不可达或者DNS 解析失败 连接被拒绝 连接过程中网络切换 Socket 被关闭所以真正请求执行以后HttpClient ↓ Engine ↓ 网络系统仍然必须处理底层异常。这就是第二道防线整个模型应该是Request ↓ NetworkConnectivityProvider ↓ 请求前检查 / \ 没网 有网 ↓ ↓ AppError.Network HttpClient ↓ Engine ↓ 真正执行网络请求 ↓ 网络仍可能突然失败 ↓ Throwable ↓ ExceptionMapper ↓ AppError.Network所以请求前检查负责快速失败真实网络异常处理负责最终兜底。二者不是二选一。五、AppError.Network 可以来自两个入口这点非常重要。业务层最终看到AppError.Network它可能来自第一种请求前检查 ↓ 明确没有互联网连接 ↓ 直接 AppError.Network请求甚至没有真正进入 Engine。第二种请求前检查正常 ↓ 开始 HTTP ↓ 网络过程中出现连接异常 ↓ Throwable ↓ ExceptionMapper ↓ AppError.Network对于 ViewModel 来说网络就是不可用大多数情况下没有必要知道到底是请求前发现的 还是请求过程中断掉的所以两个入口可以最终统一到AppError.Network这就是错误抽象的价值。六、第三类TimeoutTimeout 建议单独处理。因为Network和Timeout虽然都属于网络请求失败但业务含义并不完全一样。例如Network ↓ 当前网络不可用 / 网络连接失败而Timeout ↓ 请求已经进入网络流程 但某个阶段等待时间超过限制Ktor 的HttpTimeout主要提供三种超时Request Timeout Connect Timeout Socket Timeout官方定义分别是Request Timeout ↓ 整个 HTTP Call 从发送到接收完成所允许的时间 Connect Timeout ↓ 与服务器建立连接允许的最大时间 Socket Timeout ↓ 数据交换过程中两个数据包之间允许的最大空闲时间Ktor 对应可能抛出HttpRequestTimeoutException ConnectTimeoutException SocketTimeoutException因此sealed interface AppError { data object Network : AppError data object Timeout : AppError }是一个比较合理的基础设计。七、第四类HTTP Error如果已经拿到了HTTP Response说明 HTTP 通信至少已经走到了服务端响应阶段。但是状态码可能是401 Unauthorized 403 Forbidden 404 Not Found 500 Internal Server Error 502 Bad Gateway 503 Service Unavailable这些属于HTTP 协议层错误它和请求前断网完全不是一个层级。也和ApiResponse.code ! 0不是一个层级。八、expectSuccess true 到底负责哪一层例如HttpClient { expectSuccess true }它处理的是HTTP Status也就是说401 404 500这类状态不会继续按照普通成功 Response 处理而会进入 Ktor 的异常机制。可以理解成HttpClient ↓ 收到 HTTP Response ↓ 检查 Status ↓ 非成功状态 ↓ 抛出 HTTP 相关异常但是{ code: 10001, msg: token expired, data: null }这里的code 10001属于你自己项目的业务协议Ktor 不知道它意味着什么。所以expectSuccess ↓ 处理 HTTP Status ApiResponse.code ↓ 处理业务状态不要混淆。九、HTTP Error 可以进一步转换例如sealed interface AppError { data object Network : AppError data object Timeout : AppError data object Unauthorized : AppError data object Forbidden : AppError data object NotFound : AppError data class Server( val statusCode: Int, val message: String? null, ) : AppError }可以建立映射401 ↓ Unauthorized 403 ↓ Forbidden 404 ↓ NotFound 500 / 502 / 503 ↓ Server以后业务层when (error) { AppError.Unauthorized - { // 登录状态处理 } AppError.NotFound - { // 数据不存在 } is AppError.Server - { // 服务端异常 } else - Unit }业务层就不需要认识 Ktor HTTP 异常类。十、第五类JSON 解析失败假设HTTP 200说明 HTTP 层没问题。但是客户端期望{ code: 0, msg: , data: { id: 1001 } }服务器却返回{ code: abc, msg: , data: {} }客户端模型Serializable data class ApiResponseT( val code: Int, val msg: String, val data: T, )期望code Int实际code String于是ContentNegotiation ↓ kotlinx.serialization ↓ 反序列化失败这属于Parse / Serialization Error而不是Network也不是HTTP Server Error所以可以data object Parse : AppError十一、为什么 ParseError 要单独存在因为它通常意味着后端协议发生变化 客户端 DTO 写错 返回数据结构异常 服务端返回了非预期内容例如后端 userId String 客户端 userId Long这是客户端和服务端的数据协议不匹配而不是用户网络不好所以日志里必须能够明确看到Parse方便开发定位。十二、第六类业务错误 code ! 0上一篇我们已经讲过HTTP 200Body{ code: 20001, msg: 订单不存在, data: null }这里网络成功 ↓ HTTP 成功 ↓ JSON 成功 ↓ 业务失败所以属于Business Error可以定义data class Business( val code: Int, val message: String, ) : AppError这样HTTP 404和HTTP 200 code 20001就不会混到一起。十三、第七类Unknown Error无论我们分类得多完善都会存在当前没有识别的 Throwable所以最终需要data class Unknown( val cause: Throwable? null, ) : AppError作为兜底。但Unknown应该是最后兜底而不是所有不好判断的错误全部塞进去。否则错误分类也就失去了意义。十四、一个基础 AppError 可以这样设计sealed interface AppError { data object Network : AppError data object Timeout : AppError data object Unauthorized : AppError data object Forbidden : AppError data object NotFound : AppError data class Server( val statusCode: Int, val message: String? null, ) : AppError data object Parse : AppError data class Business( val code: Int, val message: String, ) : AppError data class Unknown( val cause: Throwable? null, ) : AppError }这已经覆盖大多数普通业务项目Network Timeout HTTP Parse Business Unknown十五、为什么 AppError 不一定直接继承 Exception这里可以把两个概念拆开。Throwable代表程序执行过程中真正抛出来的异常。而AppError代表项目最终想表达的错误语义。比如底层ConnectException UnknownHostException 平台 Socket Exception最终都可能是AppError.Network所以结构可以是底层 Throwable ↓ ExceptionMapper ↓ AppError这比把底层异常一比一复制到业务层更加稳定。十六、ExceptionMapper 是干什么的可以定义interface ExceptionMapper { fun map( throwable: Throwable, ): AppError }职责只有一个把底层 Throwable 翻译成项目能够理解的 AppError。例如Ktor Timeout Exception ↓ AppError.Timeout HTTP 401 Exception ↓ AppError.Unauthorized SerializationException ↓ AppError.Parse ApiException(code 20001) ↓ AppError.Business十七、为什么不直接在每个 get/post 里判断如果get post put delete upload download都写try { ... } catch (e: Throwable) { when (e) { ... } }就会产生大量重复。所以更合理的是NetworkClient ↓ 统一捕获 ExceptionMapper ↓ 统一解释职责NetworkClient ↓ 请求执行边界 ExceptionMapper ↓ 异常翻译边界十八、HTTP Error 怎么映射概念上when (statusCode) { 401 - { AppError.Unauthorized } 403 - { AppError.Forbidden } 404 - { AppError.NotFound } in 500..599 - { AppError.Server( statusCode statusCode, ) } else - { AppError.Unknown() } }这应该属于统一网络层而不是散落到UserApi OrderApi RobotApi里面。十九、业务 code 怎么映射上一篇已经有if ( response.code ! ApiCode.SUCCESS ) { throw ApiException( code response.code, message response.msg, ) }然后ApiException ↓ ExceptionMapper ↓ AppError.Business例如is ApiException - { AppError.Business( code throwable.code, message throwable.message, ) }于是业务协议错误也进入统一体系。二十、有些业务 code 可以继续上升成通用错误例如后端规定10001 ↓ Token 失效那么when (throwable.code) { 10001 - { AppError.Unauthorized } else - { AppError.Business( code throwable.code, message throwable.message, ) } }于是HTTP 401和HTTP 200 code 10001最终都可能成为AppError.Unauthorized业务层只关心当前认证状态失效而不用关心它究竟来源于哪一种后端实现。二十一、网络层不要决定 UI 行为例如AppError.UnauthorizedNetworkClient 不应该跳转登录页也不应该弹 Toast网络层只负责告诉上层 Unauthorized至于退出登录 刷新 Token 跳转登录 显示 Dialog由更上层决定。尤其 KMP 的commonMain更加不应该依赖 AndroidContext Toast Activity二十二、Timeout 为什么应该单独映射虽然 Timeout 最终也是请求失败但它和明确断网不是同一类状态。例如请求前 已经没网 ↓ 根本不发送 ↓ Network而有网 ↓ 请求正常进入 Engine ↓ 连接建立时间超过限制 ↓ Connect Timeout或者服务器连接成功 ↓ 长时间没有数据 ↓ Socket Timeout这些更适合AppError.Timeout因为后续重试策略 日志 用户提示都有可能不同。二十三、断网、网络失败与 Timeout 到底是什么关系tips补充篇 8.1《Ktor/KMP 断网处理为什么请求前要先判断网络状态》以前很容易形成一种简单理解断网 ↓ 请求一直等 ↓ Timeout这并不准确。实际上应该分成三种情况。第一种请求前已经明确断网NetworkConnectivityProvider ↓ 没有互联网连接 ↓ 直接 AppError.Network此时HttpClient ↓ 没有执行也就不会进入Connect Timeout Request Timeout Socket Timeout所以这种情况下断网在 Timeout 之前就已经被快速失败处理掉了。第二种请求前正常但网络执行阶段失败例如检查时有网 ↓ 开始请求 ↓ Wi-Fi 突然断开或者DNS 解析失败 连接被拒绝 网络路由不可达 Socket 连接异常底层可能很快直接抛出连接类异常然后ExceptionMapper ↓ AppError.Network这种情况下也不一定需要等到 Timeout。第三种请求进入等待状态并超过时间限制例如尝试连接服务器 ↓ 长时间建立不了连接 ↓ ConnectTimeoutException或者连接成功 ↓ 服务器长时间不返回数据 ↓ SocketTimeoutException或者整个请求超过规定时间 ↓ HttpRequestTimeoutException这才真正属于AppError.TimeoutKtor 的HttpTimeout官方正是按照request / connect / socket三类时间范围管理超时。不同 Engine 对具体 Timeout 类型的支持也并不完全一致例如当前文档中 Darwin 和 JavaScript Engine 对 connect/socket timeout 的支持就有所区别。因此正确关系应该是Request ↓ 请求前网络检查 / \ 没网 有网 ↓ ↓ AppError.Network Engine ↓ ┌───────────┴───────────┐ ↓ ↓ 网络立即失败 等待超时 ↓ ↓ AppError.Network AppError.Timeout所以断网不等于 TimeoutTimeout 也不等于断网。二者可能最终都是“请求没有成功”但发生阶段和错误语义不同。二十四、Retry 和异常体系有什么关系后面会单独讲HttpRequestRetry它首先要回答什么错误值得重试例如Network ↓ 某些情况下可以重试 Timeout ↓ 某些情况下可以重试 503 ↓ 可能重试 401 ↓ 通常应该先走认证 / Refresh 404 ↓ 一般没有意义重试 Business Error ↓ 通常也不应该自动重试所以Network Timeout HTTP Business必须先分清楚。否则 Retry 很容易变成失败了就再试几次这是有问题的。二十五、Network Pre-check 和 Retry 也有关系假设第一次请求 ↓ 网络失败 ↓ 准备 Retry如果此时设备已经明确断网NetworkConnectivityProvider ↓ No Network那么再连续Retry 1 Retry 2 Retry 3其实都没有意义。所以后面设计 Retry 时也可以考虑准备 Retry ↓ 检查当前网络条件 ↓ 明确断网 ↓ 不要继续无意义 Retry这也是请求前 Connectivity 状态检查具有价值的地方之一。不过Retry属于后面的 Plugin 专题这里只建立概念。二十六、网络错误体系不要过度细分底层可能存在DNS Failure Connection Refused Socket Closed SSL Handshake Certificate Error Route Failure Proxy Failure这些异常确实不同。但是业务层是否需要7 种不同 UI通常不需要。因此 AppError 的设计原则应该是按照业务真正需要处理的粒度抽象而不是按照底层异常类一比一复制。例如很多连接类异常 ↓ AppError.Network就已经足够。只有业务真的需要时再增加SSL Certificate Security等分类。二十七、AppError 的目的就是稳定上层假设当前 Android 使用Ktor OkHttp Engine以后可能换Ktor CIOiOSDarwin EngineWebJS / Wasm Engine不同平台底层抛出的具体异常细节可能并不完全一样。但是 commonMain 上层最好一直面对AppError.Network AppError.Timeout AppError.Unauthorized AppError.Parse所以Engine / Platform Exception ↓ ExceptionMapper ↓ AppError是 KMP 中特别有价值的一层抽象。二十八、一个 ExceptionMapper 可以这样理解先不纠结平台所有具体异常类概念上class ExceptionMapper { fun map( throwable: Throwable, ): AppError { return when { isTimeout(throwable) - { AppError.Timeout } isNetworkError(throwable) - { AppError.Network } isUnauthorized(throwable) - { AppError.Unauthorized } isNotFound(throwable) - { AppError.NotFound } isServerError(throwable) - { AppError.Server( statusCode getStatusCode( throwable, ), ) } isSerializationError( throwable ) - { AppError.Parse } throwable is ApiException - { AppError.Business( code throwable.code, message throwable.message, ) } else - { AppError.Unknown( cause throwable, ) } } } }真正项目中再根据Ktor Version Engine Android / iOS / Web处理具体异常类型。二十九、NetworkClient 怎么接入网络检查和 ExceptionMapper现在完整一点class NetworkClient( private val client: HttpClient, private val connectivityProvider: NetworkConnectivityProvider, private val exceptionMapper: ExceptionMapper, )执行逻辑可以理解成NetworkClient ↓ 先检查 Connectivity ↓ 明确断网 ↓ 是 ↓ AppError.Network 否则 ↓ HttpClient ↓ Throwable ↓ ExceptionMapper ↓ AppError也就是说NetworkConnectivityProvider负责的是请求前状态判断而ExceptionMapper负责的是真正执行失败后的 Throwable 映射两个职责不要混在一起。三十、不要在 get/post 每个函数里重复 try/catch如果get() post() put() delete()全部写一遍try { ... } catch { ... }很快又重复。所以可以抽executeRequest()例如概念上suspend inline fun reified T executeRequest( crossinline request: suspend () - HttpResponse, ): T { ensureNetworkAvailable() try { val response request() .bodyApiResponseT() if ( response.code ! ApiCode.SUCCESS ) { throw ApiException( code response.code, message response.msg, ) } return response.data } catch ( throwable: Throwable ) { if ( throwable is CancellationException ) { throw throwable } throw mapException( throwable ) } }于是GET POST PUT DELETE ↓ executeRequest ↓ Pre-check ↓ 真正请求 ↓ Response 解包 ↓ 异常统一结构就非常清楚。三十一、CancellationException 必须特别注意协程项目中CancellationException通常不是网络失败而是当前协程被取消例如页面退出 ViewModel Scope 取消 业务主动 cancel所以如果catch (Throwable)一定要注意if ( throwable is CancellationException ) { throw throwable }不要把它CancellationException ↓ AppError.Unknown否则正常的协程取消行为会被错误地当成请求失败甚至 UI 可能出现错误提示。三十二、为什么 CancellationException 要原样传播假设页面关闭 ↓ CoroutineScope cancel ↓ 网络协程取消这是正常生命周期行为不是服务器异常 网络异常 业务异常所以Cancellation ≠ AppError在大多数普通网络请求场景下应该继续向上传播协程取消信号。三十三、那 NetworkClient 应该 throw 还是返回 AppResult这里会出现两种设计。第一种异常模式suspend fun getUser(): User成功return User失败throw AppException第二种结果模式例如sealed interface AppResultout T { data class SuccessT( val data: T, ) : AppResultT data class Failure( val error: AppError, ) : AppResultNothing }然后suspend fun getUser(): AppResultUser流程成功 ↓ SuccessUser 失败 ↓ Failure(AppError)两种方式都有合理使用场景。这一篇主要先把错误从哪里产生 ↓ 怎么分类讲清楚。AppResultT可以后面继续展开。三十四、错误提示不要直接塞进 NetworkClient例如AppError.NetworkNetworkClient 不应该直接“网络不可用请检查网络连接”因为AppError负责的是错误语义而 UI中文提示 英文提示 Toast Dialog 页面状态属于UI / Presentation可以进一步AppError ↓ ErrorMessageMapper ↓ 用户文案尤其 KMP 项目需要考虑多语言和多平台这样职责更清晰。三十五、日志和用户提示也不能混比如底层解析异常Expected Int but found STRING at $.data.id这是开发日志用户不需要看到这些。所以可以分成Throwable ↓ Logging ↓ 保留开发诊断信息 Throwable ↓ ExceptionMapper ↓ AppError.Parse AppError.Parse ↓ UI ↓ “数据异常请稍后重试”日志、错误语义和用户提示是三层。三十六、完整异常链路可以这样画Request ↓ NetworkConnectivityProvider ↓ 请求前网络检查 / \ 没网 有网 ↓ ↓ AppError.Network HttpClient ↓ Engine ↓ ┌──────────────────┼─────────────────┐ ↓ ↓ ↓ 网络失败 Timeout HTTP Response ↓ ↓ ↓ AppError.Network AppError.Timeout HTTP Status ↓ ┌────────────┼───────────┐ ↓ ↓ ↓ 401 404 5xx ↓ ↓ ↓ Unauthorized NotFound Server ↓ Body ↓ JSON Parse ↓ Parse Error ↓ ApiResponseT ↓ code ! 0 ↓ Business Error ↓ data ↓ T这张图就是这篇真正需要建立的整体认知。三十七、不要把不同错误层级混在一起重点记住请求前断网 ≠ Timeout运行中网络失败 ≠ HTTP 500HTTP 500 ≠ 业务 code ! 0业务 code ! 0 ≠ JSON 解析失败JSON 解析失败 ≠ Unknown错误分类不是为了“分类好看”。而是因为后续Retry Auth Logging UI 监控 业务处理都会依赖这套错误模型。三十八、KMP 中 ConnectivityProvider 还有一个额外价值因为各个平台判断网络状态的方法不同。概念上commonMain NetworkConnectivityProvider ↑ │ ┌──────┼──────┐ ↓ ↓ ↓ Android iOS Web例如 AndroidConnectivityManager NetworkCapabilitiesiOS、Web 则有自己的平台能力。于是NetworkClient只知道connectivityProvider.isConnected()而不需要直接依赖Android ConnectivityManager这就是 KMP 中的平台能力抽象。这一部分涉及Android INTERNET / VALIDATED iOS NWPathMonitor Web navigator.onLine 状态监听 Pre-check内容比较多不在本篇展开。后面单独作为补充篇 8.1《Ktor/KMP 断网处理为什么请求前要先判断网络状态》来详细讲。三十九、最终推荐的基础 AppError现阶段可以先从sealed interface AppError { data object Network : AppError data object Timeout : AppError data object Unauthorized : AppError data object Forbidden : AppError data object NotFound : AppError data class Server( val statusCode: Int, val message: String? null, ) : AppError data object Parse : AppError data class Business( val code: Int, val message: String, ) : AppError data class Unknown( val cause: Throwable? null, ) : AppError }开始。以后真实项目出现SSL Certificate RateLimit Maintenance再根据业务需求增加。不要一开始设计几十种错误。四十、本篇总结一次网络请求失败并不只是一个Exception而是可能发生在不同阶段。第一道请求前快速失败NetworkConnectivityProvider ↓ 明确没有互联网 ↓ AppError.Network ↓ 不进入 HttpClient / Engine它的意义是已经知道请求不可能成功就不要继续做无意义的网络工作。第二道真实网络请求兜底即使请求前判断有网网络也可能随后变化所以HttpClient ↓ Engine ↓ 连接 / DNS / Socket 异常 ↓ ExceptionMapper ↓ AppError.Network仍然必须存在。TimeoutConnect Timeout Request Timeout Socket Timeout ↓ AppError.Timeout它和明确断网不是同一回事。HTTP401 403 404 5xx ↓ HTTP ErrorJSONSerialization Error ↓ AppError.ParseBusinessHTTP 200 ↓ ApiResponse.code ! 0 ↓ AppError.Business最终形成请求前状态 ↓ NetworkConnectivityProvider ↓ 明确断网 → AppError.Network 真正请求 ↓ Throwable ↓ ExceptionMapper ↓ AppError业务层看到的是Network Timeout Unauthorized Forbidden NotFound Server Parse Business Unknown而不是Ktor Exception Engine Exception Socket Exception Serialization Exception这一篇最重要的一句话可以更新成异常体系不是等请求失败以后才开始工作。对于已经明确的断网可以在请求真正进入 HttpClient/Engine 前快速失败而对于请求过程中发生的网络变化、Timeout、HTTP、解析和业务异常再通过 ExceptionMapper 统一转换成 AppError。两层共同组成完整的网络错误防线。补充篇 8.1《Ktor/KMP 断网处理为什么请求前要先判断网络状态》专门继续讲为什么要 Pre-check 为什么已经有 ExceptionMapper 还要提前判断断网 Android ConnectivityManager NetworkCapabilities INTERNET 和 VALIDATED 有什么区别 iOS 怎么判断 Web 怎么判断 KMP 怎么统一成 NetworkConnectivityProvider 放 NetworkClient 还是自定义 Client Plugin 为什么 Pre-check 仍然不能保证请求一定成功 网络恢复以后怎么办 Retry 和网络状态怎么配合下一篇《Ktor Plugin 实战Logging、HttpTimeout 与 HttpRequestRetry》继续解决Logging ↓ 怎么记录请求 / 响应 HttpTimeout ↓ Request / Connect / Socket 到底分别控制什么 HttpRequestRetry ↓ Network / Timeout / 5xx 哪些应该重试 POST ↓ 为什么不能随便 Retry ↓ 幂等性并正式把这一篇建立的Network Timeout HTTP错误分类和Retry 策略连接起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价