Как перехватить и отредактировать тело http-запроса с помощью ktor-client (KMM)

Моя задача — добавить в каждый http-запрос, который отправляется через клиент ktor, определенные параметры в тело. Когда я использовал модификацию, я легко делал это с помощью перехватчиков. В связи с переходом на КММ нам необходимо переоборудовать эти перехватчики в кТор. В настоящее время я использую okhttp3 в качестве движка, но хочу переключиться на CIO из-за его многоплатформенных возможностей. Не знаю, будет ли этот переход иметь большое значение для перехватчиков.

Я пробовал, но до сих пор это не удавалось, и сейчас я немного беспомощен, как решить эту проблему.

Вот мой подход, который не работал.

Я знаю, что у кТора тоже есть перехватчики типа ретрофита. Поэтому я создал собственный перехватчик, который включал в себя перехватчик моего сетевого менеджера. Затем я обновил тело перехватчика сетевого менеджера в контексте. Разве это не должно переопределить тело первоначального HTTP-запроса?

СетевойМодуль:

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: (Здесь я также пытался добавить новое тело к методу continueWith внутри метода установки, но это также вызвало у меня ошибки, связанные с невозможностью правильной сериализации содержимого)

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))
        }
    }
}

вам удалось сделать эту работу? и если да, не могли бы вы поделиться? :)

Eric 05.06.2024 15:50

да, почитай мой ответ ниже

Christian X 06.06.2024 17:31
1
2
349
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я исправил эту проблему, обновив ktor до последней версии (3.0.0-beta-1) и используя новые функции плагина ktors (которые можно использовать, по сути, как перехватчики).

Вот плагин, который я написал: Примечание --> Обратный вызов TransformRequestBody выполняет всю тяжелую работу и манипулирует данными тела.

actual class NetworkParamsPlugin : KoinComponent {

    private val defaultParams: Map<String, String> by lazy { buildDefaultParams() }
    private val context: Context by inject()
    private val userPreferences: DataBridge by inject()

    actual fun setup(config: HttpClientConfig<*>) {
        config.install(createClientPlugin("NetworkParamsPlugin") {
            onRequest { request, content ->
                defaultParams.forEach { (key, value) -> request.header(key, value) }
            }

            transformRequestBody { request, body, _ ->
                if (request.method.value.equals("POST", ignoreCase = true)) {
                    if (body is FormDataContent) {
                        val newBody = mergeParamsWithFormDataBody(body, defaultParams)
                        return@transformRequestBody FormDataContent(newBody)
                    }
                }


                body as OutgoingContent
            }
        })
    }

    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("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 mergeParamsWithFormDataBody(
        oldBody: FormDataContent,
        params: Map<String, String>
    ): Parameters {
        return Parameters.build {
            appendAll(oldBody.formData)
            // Add new parameters
            params.forEach { (key, value) ->
                append(key, value)
            }
        }
    }
}

Затем я добавил плагин в свой клиент ktor следующим образом:

HttpClient(clientEngine()) {
        expectSuccess = false
        install(Logging) {
            // IMPORTANT: If you change the level or logger the body in ktor will be intercepted and remove from the final response
            // Therefore, if you want to see the body in the response, you need to change the level to NONE back!!!!
            // Open issue: https://youtrack.jetbrains.com/issue/KTOR-6474/SaveBodyPlugin-Logging-plugin-consumes-response-body
            logger = Logger.DEFAULT
            level = LogLevel.NONE
        }
        install(ContentNegotiation) {
            json(json)
        }
        install(SaveBodyPlugin) {
            disabled = false
        }
        HttpResponseValidator {
            validateResponse { response ->
                errorHandlingInterceptor.handleResponse(response = response)
            }
        }
        defaultRequest {
            url(config.baseUrl)
            contentType(ContentType.Application.Json)
        }

        engine {
        }

        NetworkParamsPlugin().setup(this)

    }

Другие вопросы по теме