You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Ktor Client(KMM)拦截并修改HTTP请求体?

问题:Ktor Client POST请求体添加参数不生效

需求:给所有Ktor Client发送的HTTP POST请求的请求体添加特定参数,之前用Retrofit的Interceptor轻松实现,现在切换到KMM使用Ktor,当前用OkHttp引擎,计划换成CIO,不确定引擎切换对拦截器的影响。

尝试了自定义拦截器方案,但修改请求体后,日志显示拦截器内已成功添加参数,但最终发送的请求体还是初始参数,未生效。


NetworkModule

private fun provideKtorClient(
    config: AppConfig,
    networkParamsInterceptor: NetworkParamsInterceptor,
    json: Json,
): HttpClient {
    return HttpClient(OkHttp) {
        expectSuccess = false
        install(Logging) {
            logger = Logger.DEFAULT
            level = if (BuildConfig.DEBUG) LogLevel.ALL else LogLevel.NONE
        }
        install(ContentNegotiation) {
            json(json)
        }
        defaultRequest {
            url(config.baseUrl)
            contentType(ContentType.Application.Json)
        }
        engine {}

        install(InterceptorImpl) {
            interceptor = networkParamsInterceptor
        }
    }
}

private fun provideJson(): Json {
    return Json {
        ignoreUnknownKeys = true
        isLenient = true
        coerceInputValues = true
    }
}

自定义拦截器实现(InterceptorImpl)

interface HttpClientInterceptor {
    fun intercept(context: HttpRequestBuilder): HttpRequestBuilder
}

class InterceptorImpl(private var config: Config) {
       class Config {
        lateinit var interceptor: HttpClientInterceptor
    }

    companion object : HttpClientPlugin<Config, InterceptorImpl> {
        override val key: AttributeKey<InterceptorImpl> = AttributeKey("CustomInterceptors")

        override fun prepare(block: Config.() -> Unit): InterceptorImpl {
            val config = Config().apply(block)
            return InterceptorImpl(config)
        }

        override fun install(plugin: InterceptorImpl, scope: HttpClient) {
            scope.requestPipeline.intercept(HttpRequestPipeline.State) {
                plugin.config.interceptor.intercept(context)
            }
        }
    }

}

NetworkParamsInterceptor

class NetworkParamsInterceptor(
    private val context: Context,
    private val userPreferences: UserPreferences,
) : HttpClientInterceptor {

    private val defaultParams: Map<String, String> by lazy { buildDefaultParams() }

    @OptIn(InternalAPI::class)
    override fun intercept(context: HttpRequestBuilder): HttpRequestBuilder {
        return context.apply {
            // Set default headers
            defaultParams.forEach { (key, value) -> header(key, value) }

            // For POST requests, add default parameters to the body
            if (method.value.equals("POST", ignoreCase = true)) {
                val contentType = headers["Content-Type"]
                if(body is FormDataContent){
                    val bod = body as FormDataContent
                    bod.formData.forEach { s, strings -> println("XXX 2: $s, $strings")}
                    val newBody = mergeParamsWithJsonBody(bod, defaultParams)
                    body = newBody
                    bodyType = typeInfo<FormDataContent>()
                }
            }
        }
    }

    private fun buildDefaultParams(): Map<String, String> {
        val map = mutableMapOf<String, String>(
            "os_version" to android.os.Build.VERSION.SDK_INT.toString(),
            "device_model" to android.os.Build.MODEL,
            "hl" to java.util.Locale.getDefault().toString()
        )

        val apiKey = context.packageManager.getApplicationInfo(
            context.packageName,
            PackageManager.GET_META_DATA
        ).metaData?.getString("findpenguins.app-api.key")
        apiKey?.let { map["key"] = it }

        val appVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName
        appVersion?.let { map["app_version"] = it }

        userPreferences.getAccessToken()?.let { map["access_token"] = it }
        return map
    }

    private fun mergeParamsWithJsonBody(oldBody: FormDataContent, params: Map<String, String>): JsonObject {
        return buildJsonObject {
            // Copy existing parameters
            oldBody.formData.forEach { s, strings ->
                put(s, JsonPrimitive(strings.first()))
            }
            // Add new parameters
            params.forEach { (key, value) ->
                put(key, JsonPrimitive(value))
            }
        }
    }

TripCalendarApiService

class TripCalendarApiService(private val client: HttpClient, private val json: Json) {

    suspend fun getCalendarTrip(tripId: String): Result<TripCalendarResponse> {
        return try {
            val response = client.post {
                url {
                    path("trip", "calendar")
                    contentType(ContentType.Application.FormUrlEncoded)
                    setBody(FormDataContent(Parameters.build {
                        append("trip", tripId)
                    }))
                }
            }

            val responseBody = response.bodyAsText()

            if (response.status == HttpStatusCode.OK) {
                val tripResponse = json.decodeFromString<TripCalendarResponse>(responseBody)
                Result.success(tripResponse)
            } else {
                Result.failure(DataError.ApiError(response.status.value, Throwable(responseBody)))
            }
        } catch (e: Exception) {
            Result.failure(DataError.UnknownError(e))
        }
    }
}

解决方案

问题根源分析

  1. 拦截阶段错误:当前拦截器在HttpRequestPipeline.State阶段修改请求体,此阶段是请求准备的最后一步,请求体已经被转换为字节流,修改不会生效。
  2. 请求体类型不匹配:把FormDataContent转成JsonObject后直接赋值给body,但原请求的Content-Type是application/x-www-form-urlencoded,类型不匹配导致序列化异常,最终还是使用原请求体。
  3. 自定义Plugin处理不当:没有正确在请求 pipeline 中传递修改后的请求上下文。

修正步骤

1. 调整拦截阶段

修改InterceptorImpl的拦截阶段为HttpRequestPipeline.Transform,此阶段是请求体转换前的关键节点,修改会被后续流程处理:

override fun install(plugin: InterceptorImpl, scope: HttpClient) {
    scope.requestPipeline.intercept(HttpRequestPipeline.Transform) {
        plugin.config.interceptor.intercept(context)
        proceed() // 继续执行后续pipeline流程
    }
}

2. 修正请求体合并逻辑

在NetworkParamsInterceptor中,合并参数后保持FormDataContent类型,不要转换为JsonObject:

// 替换原mergeParamsWithJsonBody方法
private fun mergeParamsWithFormBody(oldBody: FormDataContent, params: Map<String, String>): FormDataContent {
    val newParameters = Parameters.build {
        // 复制原有参数
        oldBody.formData.forEach { key, values ->
            values.forEach { append(key, it) }
        }
        // 添加默认参数
        params.forEach { (key, value) ->
            append(key, value)
        }
    }
    return FormDataContent(newParameters)
}

// 修改intercept方法中的body处理逻辑
override fun intercept(context: HttpRequestBuilder): HttpRequestBuilder {
    return context.apply {
        // 设置默认headers
        defaultParams.forEach { (key, value) -> header(key, value) }

        if (method.value.equals("POST", ignoreCase = true)) {
            if(body is FormDataContent){
                val bod = body as FormDataContent
                val newBody = mergeParamsWithFormBody(bod, defaultParams)
                body = newBody
                // 不需要手动设置bodyType,Ktor会自动识别
            }
        }
    }
}

3. 处理JSON类型请求体(可选)

如果同时需要支持JSON类型的POST请求,可扩展拦截逻辑:

override fun intercept(context: HttpRequestBuilder): HttpRequestBuilder {
    return context.apply {
        defaultParams.forEach { (key, value) -> header(key, value) }

        if (method.value.equals("POST", ignoreCase = true)) {
            when(val currentBody = body) {
                is FormDataContent -> {
                    body = mergeParamsWithFormBody(currentBody, defaultParams)
                }
                is TextContent -> {
                    // 处理JSON请求体
                    val jsonObject = json.parseToJsonElement(currentBody.text).jsonObject
                    val newJsonObject = buildJsonObject {
                        jsonObject.forEach { put(it.key, it.value) }
                        defaultParams.forEach { (k, v) -> put(k, JsonPrimitive(v)) }
                    }
                    body = TextContent(newJsonObject.toString(), ContentType.Application.Json)
                }
            }
        }
    }
}

4. 引擎切换注意事项

切换到CIO引擎时,上述拦截逻辑无需大幅修改:

  • CIO引擎完全支持Ktor的请求pipeline机制,自定义拦截器逻辑通用
  • 注意CIO的配置差异(如超时、连接池),但不影响拦截器功能

内容的提问来源于stack exchange,提问作者Christian X

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 02:45:55