mcpField
K
kmp-ktor

kmp-ktor

github.com/rcosteira79开发165146 次安装6 周前更新
代码仓库

关于 kmp-ktor

面向 Kotlin Multiplatform 和 Android 项目的 Ktor 客户端配置参考技能。内容聚焦插件顺序、令牌刷新、序列化和测试等易出错细节,而非客户端基础设置。

主要功能

  1. 01Ktor 插件安装顺序
  2. 02序列化默认值配置
  3. 03Bearer 令牌刷新处理
  4. 04SSE 与 WebSocket 转换器
  5. 05错误映射与 MockEngine 测试

适用场景

  1. 01排查超时请求未重试的问题
  2. 02避免刷新令牌请求陷入循环
  3. 03测试共享的 Ktor 客户端配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rcosteira79/android-skills --skill kmp-ktor

安装后在 Claude Code 中输入/kmp-ktor确认已加载。

详细介绍

Ktor Client for KMP and Android

本文介绍 Ktor 客户端配置中容易踩的坑——插件安装顺序、序列化标志、身份验证刷新和错误映射;不涉及共享 HttpClient、引擎选择或 ContentNegotiation 设置的基础知识。相关内容: android-skills:android-data-layer(仓库模式及 DataError 错误模型——其规范归属位置)、android-skills:android-retrofit(仅适用于 Android 的对应方案)。

插件安装顺序——HttpRequestRetry 要在 HttpTimeout 之前

最常见的错误安装顺序是:先安装 HttpTimeout,再安装 HttpRequestRetry。对于发出的请求,插件会按安装顺序运行;重试必须能捕获超时错误,因此重试插件必须包裹超时插件。

val json = Json { ignoreUnknownKeys = true; coerceInputValues = true; encodeDefaults = true }  // encodeDefaults: see the section below

HttpClient(engine) {
    install(ContentNegotiation) { json(json) }
    install(Auth) { bearer { /* loadTokens / refreshTokens */ } }
    install(HttpRequestRetry) {                 // BEFORE HttpTimeout
        retryOnServerErrors(maxRetries = 3)
        exponentialDelay()
    }
    install(HttpTimeout) {                       // AFTER HttpRequestRetry
        requestTimeoutMillis = 30_000; connectTimeoutMillis = 15_000; socketTimeoutMillis = 15_000
    }
}

如果顺序反过来,HttpTimeout 会在重试插件看到请求之前将其判定为失败,因此超时永远不会重试。(另外,Auth 插件会独立处理 401 刷新——让 HttpRequestRetry 处理瞬时错误和 5xx 错误;不要让两者对同一个状态码进行链式处理。)

encodeDefaults = true——否则协议常量字段会悄然消失

kotlinx.serialization 默认使用 encodeDefaults = false,会从序列化输出中剔除所有值等于其声明默认值的属性。jsonrpc: String = "2.0"(或 version = "1.0"、type = "...")会从负载中消失;服务端会以通用的“invalid request”拒绝每个请求,而修复方法只是添加一行标志——但往往要花数小时排查 HTTP 层的误导性线索后才能找到。客户端 API 始终应启用此选项——本文件顶部定义的 val json 就启用了它,同时还设置了 ignoreUnknownKeys 和 coerceInputValues。整个客户端共享的就是这个已配置实例:install(ContentNegotiation) { json(json) } 和 WebSocket 转换器都会使用它。

expectSuccess——始终一致地选择一种模式

expectSuccess = true 会使 Ktor 在收到非 2xx 响应时抛出 ClientRequestException(4xx)或 ServerResponseException(5xx)——而且该异常会在任何手动状态检查之前抛出,因此之后的 if (response.status == OK) 分支永远不会执行。应在整个项目中统一采用一种模式:expectSuccess = true + try/catch(符合仓库模式),或 expectSuccess = false + 显式检查 response.status.isSuccess()。切勿混用。

Bearer 刷新——使用 markAsRefreshTokenRequest(),否则会陷入循环

在 Auth 的 bearer { refreshTokens { … } } 代码块中,使用 markAsRefreshTokenRequest() 标记刷新 POST 请求,使其不会被同一个 Auth 插件拦截——如果不标记,刷新失败会触发另一次刷新,导致无限循环。它是一个 HttpRequestBuilder 扩展函数:要在请求构建器代码块内调用,不能直接放在 refreshTokens { } 中(否则无法编译)。

install(Auth) {
    bearer {
        loadTokens { tokenStorage.getTokens()?.let { BearerTokens(it.access, it.refresh) } }
        refreshTokens {
            val refresh = oldTokens?.refreshToken ?: return@refreshTokens null
            val r = client.post("auth/refresh") {
                markAsRefreshTokenRequest()                      // skip the Auth plugin for this call
                setBody(RefreshRequestDto(refresh))
            }.body<TokenResponseDto>()
            tokenStorage.save(r.accessToken, r.refreshToken); BearerTokens(r.accessToken, r.refreshToken)
        }
        sendWithoutRequest { it.url.pathSegments.none { seg -> seg in listOf("login", "register") } }
    }
}

在插件边界处使用 BearerTokens;应用其余部分使用自定义的令牌类型。TokenStorage 由项目定义(Android/JVM 上使用 DataStore,iOS 上使用 Keychain)。

WebSocket 和 SSE——使用序列化转换器

对于实时传输,安装 kotlinx-serialization 转换器,让类型化消息使用与 ContentNegotiation 相同的 Json 配置;否则就得手动对 Frame.Text 进行编码和解码。(SSE = 仅服务端到客户端,使用普通 HTTP,内置重连;WebSocket = 双向通信,需手动重连,支持二进制帧——如果客户端只负责接收,默认使用 SSE。)

val client = HttpClient(engine) {
    install(WebSockets) {
        pingIntervalMillis = 30_000
        contentConverter = KotlinxWebsocketSerializationConverter(json)  // the shared configured instance — bare `Json` reverts to encodeDefaults = false
    }
    install(SSE)
}

client.webSocket("wss://api.example.com/ws") {
    sendSerialized(SubscribeMessage(topic = "items"))
    while (true) { val msg = receiveDeserialized<ServerMessage>(); /* handle */ }
}

// SSE — incoming is a Flow<ServerSentEvent>
client.sse("https://api.example.com/events") { incoming.collect { event -> /* event.event / event.data / event.id */ } }

将 SSE/WebSocket 收集操作封装在 LaunchedEffect 或仓库协程中,这样消费者离开时,取消操作会关闭 HTTP 连接。

错误映射与测试

在仓库层捕获特定的 Ktor 类型(ClientRequestException / ServerResponseException / HttpRequestTimeoutException / IOException),并映射到 DataError——catch (e: Exception) 会吞掉 CancellationException。完整的仓库模式和 DataError 分类体系位于 android-skills:android-data-layer。对于更丰富的逐错误 UI 状态(Unauthorized、RateLimited、Forbidden 等),另一种方案是使用密封类型 ApiResult<T>,并配合 expectSuccess = false 的 safeRequest 包装器——每个项目选择一种即可。

注入 HttpClientEngine,让测试能够切换到 MockEngine,并复用生产环境的 createHttpClient 工厂,以确保插件配置一致:

val mockEngine = MockEngine { request ->
    assertEquals("/users/42", request.url.encodedPath)
    respond("""{"id":"42","name":"Ada","created_at":1700000000000}""",
        HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json"))
}
val repo = UserRepository(UserService(createHttpClient(mockEngine, baseUrl = "https://api.example.com/")))

评论

支持 Markdown