前面几篇,我已经把 Ktor 网络层逐渐搭到了这里:
ApiService ↓ NetworkClient ↓ NetworkConnectivityProvider ↓ Pre-check ↓ HttpClient ↓ Engine ↓ HTTP同时也建立了异常体系:
Network Timeout HTTP Parse Business Unknown ↓ ExceptionMapper ↓ AppError现在一个请求已经能够:
发送 Request ↓ 接收 Response ↓ 解析 JSON ↓ 检查业务 code ↓ 统一异常但一个真正可以投入项目使用的网络层,还需要三个非常重要的能力:
Logging ↓ 这次 HTTP 请求到底发生了什么? HttpTimeout ↓ 这次请求最多允许等待多久? HttpRequestRetry ↓ 请求失败以后,什么时候值得再试一次?这三个能力都可以通过 Ktor Client Plugin 来完成。
截至当前 Ktor 官方文档 3.5.2,这三个能力分别对应Logging、HttpTimeout和HttpRequestRetry。
这一篇不打算把每个 Plugin 的所有 API 都列一遍。
真正要搞清楚的是:
Logging、Timeout、Retry 在一次 Request 生命周期中分别承担什么职责,以及它们之间如何配合。
一、先重新理解 Plugin
前面已经讲过:
HttpClient + Plugin + Engine是 Ktor Client 很重要的设计。
例如:
val client = HttpClient { install(Logging) install(HttpRequestRetry) install(HttpTimeout) }可以理解成:
HttpClient │ ├── Logging │ ↓ │ 增加 HTTP 日志能力 │ ├── HttpRequestRetry │ ↓ │ 增加失败重试能力 │ └── HttpTimeout ↓ 增加超时控制能力Plugin 不是:
UserApi 调一次 OrderApi 再调一次 RobotApi 再调一次而是:
安装到某一个 HttpClient 上,为这个 Client 增加统一网络能力。
所以它首先是:
Client Scope的配置。
二、三个 Plugin 不要混在一起
先把三个问题彻底分开。
Logging
回答:
发生了什么?例如:
请求 URL 是什么? Method 是什么? Header 有没有? 服务器返回什么 Status? Response 是什么?HttpTimeout
回答:
最多允许等多久?HttpRequestRetry
回答:
失败以后,要不要重新发一次?所以:
Logging ≠ Timeout ≠ Retry可以简单记:
Logging 负责看见,Timeout 负责限制等待,Retry 负责决定是否再试。
三、第一个 Plugin:Logging
安装最简单:
install(Logging)Ktor 官方的LoggingPlugin 用于记录 HTTP Call,并且可以配置:
logger level filter sanitizeHeader bodyFilter其中LoggingConfig当前明确提供logger、level、filter()、sanitizeHeader()和bodyFilter。
四、最简单的系统 Logging
例如:
install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL }这里:
Logging Plugin ↓ 采集 HTTP Request / Response 信息 ↓ Logger.DEFAULT ↓ 输出日志在 JVM 平台,Logger.DEFAULT使用 SLF4J;Android 官方文档推荐配合slf4j-android。Ktor Multiplatform 也支持自己提供 Logger。
五、一个真实 Logging 例子
假设:
POST /login真实 Request:
POST /login Content-Type: application/json Authorization: Bearer abc123 { "username": "tom", "password": "123456" }配置:
install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL sanitizeHeader { headerName -> headerName == HttpHeaders.Authorization } }那么日志中:
Authorization: ***真实 Token 不再直接出现。
而真正发给服务器的 Request 仍然是:
Authorization: Bearer abc123也就是说:
sanitizeHeader ↓ 只影响日志展示 不会修改真实 HttpRequestKtor 官方当前的sanitizeHeader()就是用来把指定敏感 Header 的日志值替换掉,默认占位符为***。
六、这里马上出现一个问题:password 怎么办?
刚才 Request Body:
{ "username": "tom", "password": "123456" }虽然:
Authorization ↓ ***但是:
password仍然在 Body 中。
因为:
sanitizeHeader只处理:
Header不能处理:
JSON BodyKtor 3.5.x 的LoggingConfig还提供了:
bodyFilter而LogBodyFilter当前同时有filterRequest()和filterResponse(),可以决定 Request/Response Body 如何进入日志,例如脱敏、截断或者跳过。
这一篇不继续展开 Body 脱敏,否则 Logging 会占掉整篇文章。
Tips:
sanitizeHeader、bodyFilter、JSON Body 脱敏、Binary/Multipart 跳过、大 Body 截断、Logger.DEFAULT与 Custom Logger 的完整设计,放到补充篇9.1《Ktor Logging 深入:Header、Body 脱敏与自定义 Logger》单独讲。
七、Logger.DEFAULT 和 Custom Logger 有什么区别?
这个主篇只建立基本认识。
方案 A:
install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL sanitizeHeader { it == HttpHeaders.Authorization } }流程:
Ktor Logging ↓ LoggingConfig ↓ 脱敏 / 过滤 ↓ Logger.DEFAULT ↓ 平台日志系统方案 B:
install(Logging) { logger = object : Logger { override fun log( message: String, ) { AppLogger.d( tag = "HTTP", message = message, ) } } level = LogLevel.ALL sanitizeHeader { it == HttpHeaders.Authorization } }流程:
Ktor Logging ↓ LoggingConfig ↓ 脱敏 / 过滤 ↓ Custom Logger ↓ AppLogger / Kermit所以:
两种方式都可以使用 Ktor 自带的脱敏能力。
区别主要在:
Logger.DEFAULT ↓ 使用默认日志出口 Custom Logger ↓ 把 Ktor 日志接入自己的日志系统Ktor 官方同样支持通过实现Logger.log(message)提供 Custom Logger。
八、Logging 在架构中应该负责什么?
最终可以理解:
Logging Plugin ↓ 采集 HTTP 信息 LoggingConfig ↓ 决定记录什么 ↓ level filter sanitizeHeader bodyFilter Logger ↓ 决定日志输出到哪里所以:
ApiService不应该:
println("POST /login")到处自己打印网络日志。
统一 HTTP 日志应该属于:
HttpClient ↓ Logging Plugin九、第二个 Plugin:HttpTimeout
接下来是:
HttpTimeout它解决的问题非常简单:
网络请求不能无限等下去。
例如:
install(HttpTimeout) { requestTimeoutMillis = 15_000 connectTimeoutMillis = 10_000 socketTimeoutMillis = 15_000 }Ktor 官方当前把 Timeout 分为:
Request Timeout Connect Timeout Socket Timeout三类。
十、Connect Timeout
首先:
Connect Timeout负责:
建立服务器连接最多允许等待多久。
例如:
GET /orders ↓ 尝试连接 api.example.com ↓ 一直连接不上 ↓ 超过 10 秒 ↓ ConnectTimeoutException如果:
connectTimeoutMillis = 10_000意思就是:
建立连接 ↓ 最多等 10 秒可以记成:
Connect Timeout = 门一直敲不开,我最多等多久。
十一、Socket Timeout
假设:
连接已经成功但是:
服务器迟迟没有继续发送数据这时候关注:
Socket TimeoutKtor 官方把它定义成:
数据交换过程中,两个数据包之间允许的最大无数据活动时间。
例如:
连接成功 ↓ 服务器开始返回数据 ↓ 突然 20 秒没有任何数据 ↓ Socket Timeout可以记:
Socket Timeout = 已经接通了,但你多久不说话我就不等了。
十二、Request Timeout
最后:
Request Timeout范围最大。
它关注:
发送 Request ↓ 一直到接收 Response整笔 HTTP Call 所允许的时间。
可以画成:
Request Timeout ┌──────────────────────────┐ ↓ ↓ Request → Connect → 数据交换 → Response ↑ ↑ │ │ Connect Socket Timeout Timeout可以记:
Request Timeout = 整笔请求最多允许花多久。
十三、一个 Timeout 实际例子
配置:
install(HttpTimeout) { requestTimeoutMillis = 15_000 connectTimeoutMillis = 10_000 socketTimeoutMillis = 15_000 }请求:
GET /orders情况一:
2 秒建立连接 ↓ 3 秒返回数据 ↓ 总计 5 秒 ↓ 成功情况二:
开始连接 ↓ 超过 10 秒仍然建立不了连接 ↓ Connect Timeout情况三:
2 秒连接成功 ↓ 服务器开始返回 ↓ 中途超过 15 秒没有数据 ↓ Socket Timeout情况四:
连接 + 数据传输 ↓ 整笔 Call 超过 Request Timeout ↓ Request Timeout这样三个 Timeout 就比较容易区分了。
十四、Timeout 也存在“Client 默认值 + Request 特殊值”
例如普通接口:
GET /orders GET /users GET /products统一:
install(HttpTimeout) { requestTimeoutMillis = 15_000 }但是上传:
POST /upload可能明显需要更长时间。
于是:
client.post("upload") { timeout { requestTimeoutMillis = 120_000 } }这一次 Request:
120 秒普通 Request:
15 秒Ktor 官方当前明确支持 Request 级 timeout,并且 Request 配置会覆盖 Client Plugin 的全局对应 timeout。
这正好又对应我们前面讲过的:
Client 默认配置 ↓ 大多数 Request Request 特殊配置 ↓ 只影响当前请求十五、Timeout 还存在 Engine 差异
KMP 这里要特别注意。
commonMain 可以统一配置:
Request Connect Socket但不同 Engine 的底层能力不一定完全一致。
例如当前 Ktor 官方文档中:
Darwin Request ✅ Connect ❌ Socket ✅ JavaScript Request ✅ Connect ❌ Socket ❌所以:
KMP 可以统一 Timeout 配置思想,但不能理解成所有平台 Engine 的底层 Timeout 能力完全一致。
这和 ConnectivityProvider 的道理一样:
统一的是抽象 ≠ 所有平台实现完全相同十六、Timeout 最终怎么进入异常体系?
Ktor 当前可能抛:
HttpRequestTimeoutException ConnectTimeoutException SocketTimeoutException如果业务没有必要区分这么细:
三种 Ktor Exception ↓ ExceptionMapper ↓ AppError.Timeout即可。
ViewModel 不需要:
catch ( e: SocketTimeoutException )十七、第三个 Plugin:HttpRequestRetry
接下来进入最容易写出问题的:
HttpRequestRetry它解决:
这次 Request 失败以后,要不要重新发送一次?
首先有一个非常重要的前提:
请求失败,不等于应该 Retry。
Ktor Client 默认不会因为网络或服务器错误自动替你反复请求;安装HttpRequestRetry后才可以配置 Retry 次数、条件、Delay 以及 Retry 前如何修改 Request。
十八、先看最简单的 Retry
例如:
install(HttpRequestRetry) { retryOnServerErrors( maxRetries = 2, ) exponentialDelay() }这里:
5xx ↓ 允许 Retry maxRetries = 2 ↓ 最多重试 2 次 exponentialDelay() ↓ Retry 之间不要立即连续请求Ktor 官方retryOnServerErrors()当前就是用于 5xx Response 重试,而exponentialDelay()用于指数退避。
十九、一个 503 Retry 的真实例子
例如:
GET /products第一次:
Request #1 ↓ 503 Service Unavailable服务器可能正在:
瞬时负载过高 滚动发布 网关临时异常于是:
等待 ↓ Retry第二次:
Request #2 ↓ 200 OK整个流程:
GET /products ↓ 503 ↓ HttpRequestRetry ↓ 等待 ↓ GET /products ↓ 200 ↓ 继续正常业务流程这时候:
ViewModel甚至根本不需要知道:
第一次出现过 503这就是 Retry 的一个重要价值:
在最终错误暴露给业务层之前,消化某些瞬时故障。
二十、为什么不能所有 5xx 都无脑 Retry?
因为 Retry 不只是:
Response 出错了吗?还要问:
这笔 Request 再发一次安全吗?这就涉及:
幂等性二十一、最重要的 Retry 反例:创建订单
例如:
POST /orders ↓ 创建订单Client 发送:
POST /orders服务器:
已经创建 Order #1001然后 Response 返回途中:
网络断开Client 最终看到:
Network Error或者:
TimeoutClient 很容易误认为:
“请求失败了”然后自动 Retry:
POST /orders服务器可能:
再创建 Order #1002最后:
用户操作一次 ↓ 产生两张订单所以非常重要的一句话是:
客户端没有收到成功 Response,不代表服务端没有执行成功。
二十二、这就是为什么 Retry 必须考虑幂等性
简单理解:
同一个请求执行一次和执行多次,最终业务结果应该一致。
例如规范的:
GET /users多执行几次:
只是查询通常风险比较低。
而:
POST /orders多执行一次可能产生:
第二张订单所以:
失败 ↓ 不能直接推导出 ↓ Retry而应该:
失败 ↓ 错误是否值得重试? ↓ Request 能安全重放吗? ↓ 都满足 ↓ Retry二十三、POST 怎么才能安全 Retry?
一种常见设计:
Idempotency-Key例如:
Idempotency-Key: order-100001第一次:
POST /orders Key = ABC ↓ 服务器创建 Order #1001 ↓ 记录 ABC 已经处理Retry:
POST /orders Key 仍然 = ABC服务器:
发现 ABC 已经处理 ↓ 不创建第二张订单 ↓ 返回原结果这时候:
客户端 Retry + 服务端幂等才真正形成一套可靠机制。
所以:
HttpRequestRetry 只能负责“再发送”,真正能不能安全再发送,还取决于业务协议设计。
二十四、机器人控制指令更不能无脑 Retry
例如:
向前移动 1 米Client:
Command #1001 ↓ Server / Robot ↓ 已经执行但:
Ack 丢失Client:
没收到 ↓ Retry如果 Retry 被当成:
新命令机器人可能:
再移动 1 米所以机器人领域通常还需要:
CommandId Sequence Ack Result 幂等处理HttpRequestRetry 本身无法替你解决这些业务协议问题。
二十五、Retry 判断最好拆成三个问题
以后看到任何 Retry,都可以问:
第一问
这个错误值得 Retry 吗?例如:
Network Error Timeout 503可能值得。
而:
404 Parse Error Business Error通常没有意义自动 Retry。
第二问
当前还有网络条件吗?也就是:
NetworkConnectivityProvider ↓ Available ?第三问
Request 能安全重放吗?例如:
GET 幂等 PUT Idempotency-Key Command Sequence最终:
Retryable Error AND Connectivity Available AND Request Safe ↓ Retry这是比:
maxRetries = 3重要得多的 Retry 模型。
二十六、Retry 也可以根据 Exception 判断
Ktor 当前支持:
retryOnExceptionIf { request, cause, -> ... }官方示例就是根据 Request 和 Throwable 自定义是否 Retry。
例如:
retryOnExceptionIf( maxRetries = 2, ) { request, cause -> val safeRequest = request.method == HttpMethod.Get val networkAvailable = connectivityProvider .isNetworkAvailable val retryableError = isRetryableNetworkException( cause ) safeRequest && networkAvailable && retryableError }这个例子非常重要。
因为 Retry 已经不只是:
Exception ↓ Retry而是:
Exception + Request + Provider 当前状态 ↓ 共同决定二十七、这里正好连接 8.1 的动态 Provider
补充篇 8.1:Ktor/KMP 断网处理:为什么请求前要先判断网络状态?
第一次 Request:
Provider = Available ↓ NetworkClient Pre-check ↓ 通过 ↓ HttpClient请求失败:
Timeout但是准备 Retry 时:
Wi-Fi 已经断开 ↓ Provider = Unavailable这时候:
retryOnExceptionIf ↓ 再次读取 Provider ↓ false ↓ 停止 Retry流程:
Request #1 ↓ Provider = Available ↓ 开始请求 ↓ Timeout ↓ 准备 Retry ↓ 读取同一个 Provider ↓ Provider 已经变成 Unavailable ↓ 不 Retry这就真正体现了:
HttpClient 持有的是动态 Provider 对象,不是创建 Client 时的一次 Boolean 快照。
二十八、为什么 NetworkClient 的 Pre-check 不够?
因为现在结构:
NetworkClient ↓ Pre-check ↓ HttpClient ↓ HttpRequestRetryPre-check:
只在第一次进入 HttpClient 之前执行但 HttpRequestRetry:
是在 HttpClient 内部重新发送所以:
Request #1 ↓ 经过 NetworkClient Pre-check Retry #1 ↓ 不会天然重新回到 NetworkClient Pre-check因此如果你希望:
每次 Retry ↓ 都考虑最新 ConnectivityRetry Condition 自己重新读取:
NetworkConnectivityProvider是比较自然的方案。
二十九、Retry Delay 为什么不能省?
假设服务器:
503你马上:
Retry Retry Retry可能会:
服务器已经压力很大 ↓ 客户端继续猛打 ↓ 情况更差所以 Retry 通常要:
Backoff例如:
第一次等待 1 秒 第二次等待更久 第三次继续增加Ktor 当前官方基础示例提供:
exponentialDelay()用于指数退避。
核心思想:
瞬时错误值得再试,但不要立即连续轰炸服务器。
三十、HttpRequestRetry 和 HttpTimeout 的安装顺序
这里有一个 Ktor 当前明确要求的细节。
如果:
HttpRequestRetry + HttpTimeout一起使用,并且希望 Retry 能够配置处理 Timeout:
HttpClient { install(HttpRequestRetry) { ... } install(HttpTimeout) { ... } }也就是:
HttpRequestRetry ↓ 先 install HttpTimeout ↓ 后 installKtor 3.5.2 官方文档明确说明了这个顺序。
这里不要按照:
“哪个先发生就哪个先 install”去推理。
Plugin 安装顺序和 Ktor Pipeline 的组合有关。
这个场景直接按官方要求即可。
三十一、Timeout + Retry 的实际例子
假设:
GET /products配置:
Request Timeout = 15 秒 maxRetries = 2第一次:
Request #1 ↓ Timeout准备 Retry:
Provider = Available ↓ GET ↓ Retry Policy 允许 ↓ 等待第二次:
Request #2 ↓ 200那么最终:
业务层 ↓ 成功但这里要注意:
单次 Request Timeout和:
整个业务操作总耗时不是一个概念。
因为可能:
Attempt #1 ↓ Timeout Backoff Attempt #2 ↓ Timeout Backoff Attempt #3所以整个操作的总时间:
可能明显超过一次 requestTimeoutMillis如果某个业务有严格的:
整个操作最多 20 秒还可能需要业务层额外的总 Deadline,而不能只依赖单次 HttpTimeout。
三十二、一个比较保守的 Retry 例子
项目早期可以先从:
GET + 5xx开始。
例如:
install(HttpRequestRetry) { maxRetries = 2 retryIf { request, response -> val safeRequest = request.method == HttpMethod.Get val retryableStatus = response.status.value in 500..599 val networkAvailable = connectivityProvider .isNetworkAvailable safeRequest && retryableStatus && networkAvailable } exponentialDelay() }这个配置真正表达的是:
GET AND 5xx AND 当前仍然有网络 ↓ Retry而不是:
5xx ↓ 统统 Retry三十三、Logging、Timeout、Retry 现在可以放回一条链路
Request ↓ Connectivity Pre-check ↓ HttpClient │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ Logging HttpTimeout HttpRequestRetry ↓ ↓ ↓ 看见过程 时间边界 Retry Policy │ │ │ │ │ ┌──────┼──────┐ │ │ ↓ ↓ ↓ │ │ Error Network Request │ │ State Safety │ │ ↓ └──────────────┴────────────────┘ ↓ Engine ↓ HTTP一句话:
Logging ↓ 让我知道发生了什么 Timeout ↓ 让我不要无限等待 Retry ↓ 让我在值得的时候再试一次三十四、它们和 ExceptionMapper 是什么关系?
假设:
Request ↓ Timeout如果:
Retry Policy ↓ 允许 Retry先继续 Retry。
直到:
成功或者:
Retry 用完 ↓ 仍然失败才:
Throwable ↓ ExceptionMapper ↓ AppError.Timeout所以:
Retry负责的是:
在错误最终暴露给上层前,尝试恢复瞬时故障。
而:
ExceptionMapper负责的是:
最终仍然失败时,把底层异常翻译成稳定的 AppError。
三十五、一个阶段性的 HttpClient 配置
把三个 Plugin 放在一起,可以先写成:
fun createHttpClient( connectivityProvider: NetworkConnectivityProvider, ): HttpClient { return HttpClient { install(HttpRequestRetry) { maxRetries = 2 retryIf { request, response, -> val safeRequest = request.method == HttpMethod.Get val retryableStatus = response.status.value in 500..599 safeRequest && retryableStatus && connectivityProvider .isNetworkAvailable } retryOnExceptionIf { request, cause, -> val safeRequest = request.method == HttpMethod.Get val retryableError = isRetryableNetworkException( cause ) safeRequest && retryableError && connectivityProvider .isNetworkAvailable } exponentialDelay() } install(HttpTimeout) { requestTimeoutMillis = 15_000 connectTimeoutMillis = 10_000 socketTimeoutMillis = 15_000 } install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL sanitizeHeader { headerName, -> headerName == HttpHeaders.Authorization } } } }注意:
这只是一个:
阶段性示例不是:
所有项目的最终标准答案例如正式项目的 Logging 可能接:
Kermit AppLogger 文件日志 统一脱敏Retry 也可能根据不同 Client 使用不同策略。
三十六、三个 Plugin 也有 Client 作用域
例如:
apiClient可能:
Logging = Debug Timeout = 15s GET Retry = 2而:
uploadClient可能:
Logging ↓ 不打印二进制 Body Timeout ↓ 120s Retry ↓ 使用上传自己的策略机器人 Client:
robotClient可能:
Timeout ↓ 更短 Retry ↓ 不能只看 HTTP ↓ 还要考虑 CommandId / Ack所以仍然是:
配置不是越集中越好,而是应该放到正确的 Client 作用域。
三十七、本篇最容易踩的几个坑
坑一:Logging 全部LogLevel.ALL直接上正式环境
可能泄露:
Authorization Cookie Password Token 用户隐私至少需要:
sanitizeHeader bodyFilter 日志环境控制更完整方案见 9.1。
补充篇 8.1:Ktor/KMP 断网处理:为什么请求前要先判断网络状态?
坑二:认为 Custom Logger 才能脱敏
不对。
Logger.DEFAULT和:
Custom Logger都可以使用:
sanitizeHeader bodyFilter因为它们属于:
LoggingConfig而不是 Logger 本身。
坑三:把断网当 Timeout
不对。
请求前明确断网 ↓ Network 等待超过限制 ↓ Timeout坑四:所有 Timeout 都 Retry
不对。
还需要考虑:
当前网络状态 Request 是否安全重放 业务是否允许坑五:POST Timeout 自动 Retry
非常危险。
因为:
Client 没收到 Response不代表:
Server 没执行坑六:Retry 越多越稳定
不对。
过多 Retry 可能:
增加等待 增加服务器压力 制造重复业务Retry 是解决:
瞬时故障不是把:
所有失败硬磨成成功。
坑七:只在 NetworkClient 做 Connectivity Pre-check
第一次请求够用。
但 HttpRequestRetry:
在 HttpClient 内部 Retry不会天然重新回到:
NetworkClient Pre-check所以 Retry Condition 如果需要 Connectivity:
重新读取 Provider 当前状态更加合理。
坑八:忽略 Retry 和 Timeout 的安装顺序
需要 Retry Timeout 时:
HttpRequestRetry ↓ 先安装 HttpTimeout ↓ 后安装这是 Ktor 当前官方明确要求。
三十八、本篇总结
这一篇最重要的是把三个 Plugin 的职责彻底分开。
Logging
回答:
发生了什么?结构:
Logging Plugin ↓ LoggingConfig │ ├ level ├ filter ├ sanitizeHeader └ bodyFilter ↓ LoggerLogger.DEFAULT和 Custom Logger 都可以使用 Ktor 自带的过滤/脱敏配置;Custom Logger 只是把处理后的日志接入自己的日志系统。
Tips:完整日志方案见补充篇 9.1《Ktor Logging 深入:Header、Body 脱敏与自定义 Logger》。
HttpTimeout
回答:
允许等多久?三种:
Connect Timeout ↓ 建立连接最多多久 Socket Timeout ↓ 数据交换过程中 最大无活动时间 Request Timeout ↓ 整笔 HTTP Call 最多多久并且:
Client 默认 Timeout ↓ 可以被 Request 特殊 Timeout 覆盖HttpRequestRetry
回答:
失败以后 是否值得再发送一次?真正可靠的 Retry 不是:
失败 ↓ Retry而是:
错误值得 Retry? ↓ 当前网络 Available? ↓ Request 可以安全重放? ↓ 三者都满足 ↓ Retry再配合:
有限次数 + Backoff处理瞬时故障。
把目前的整个网络可靠性链路串起来:
Request ↓ Connectivity Pre-check ↓ Logging ↓ HttpTimeout ↓ HttpRequestRetry ↓ Engine ↓ HTTP ↓ Response / Throwable ↓ 必要时 Retry ↓ 最终仍失败 ↓ ExceptionMapper ↓ AppError这一篇真正需要记住四句话:
Connectivity 解决“现在是否值得请求”。
Logging 解决“这次请求到底发生了什么”。
Timeout 解决“我最多愿意等多久”。
Retry 解决“失败以后,这笔请求是否值得而且能够安全地再试一次”。
当这几个职责分开以后,网络层就不再是:
client.get() ↓ 成功 / 失败这么简单,而开始具备真正的工程可靠性。
补充篇 9.1
《Ktor Logging 深入:Header、Body 脱敏与自定义 Logger》
专门展开:
LoggingConfig 和 Logger 的关系 Logger.DEFAULT Custom Logger sanitizeHeader bodyFilter Request / Response Body 脱敏 JSON 敏感字段递归处理 Binary / Multipart Skip 大 Body 截断 Kermit / AppLogger 对接 项目级二次脱敏 最终完整生产级 Logging 示例下一篇
《Ktor Auth:Bearer Token、Refresh Token 与 401 自动刷新到底怎么工作?》
下一篇开始解决:
Access Token ↓ 怎么自动加到 Request? 401 ↓ 发生以后谁处理? Refresh Token ↓ 为什么经常需要独立 Client? Refresh 成功 ↓ 原来的 Request 怎么重新发送? 10 个请求同时 401 ↓ 为什么不能刷新 10 次 Token? TokenProvider ↓ 为什么又是一个动态 Provider? Auth Plugin ↓ 和 DefaultRequest 手动添加 Authorization 到底有什么区别?并把之前的:
apiClient refreshClient TokenProvider 401 Provider 动态状态 Retry真正连接起来。