Моя задача — добавить в каждый 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))
}
}
}
да, почитай мой ответ ниже
Я исправил эту проблему, обновив 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)
}
вам удалось сделать эту работу? и если да, не могли бы вы поделиться? :)