Kotlin Multiplatform Mobile: Intercepting Network Request and Response

Most of Android developers nowadays know about OkHttp Interceptors, which help to implement many widespread things related to network operations as logging response body, adding header parameters to all requests, handling errors and etc. Most importantly all these things are done in one place, so that you can track bugs and make changes without additional effort.

OkHttpClient.Builder().apply {
writeTimeout(60, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
addInterceptor(HeadersInterceptor(get()))
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
addInterceptor(HandleErrorInterceptor(get(), get(), get(), get()))}.build()

As calls to remote server API are the core thing in 90+ percent of modern apps, it is natural that you need the same things when changing technology stack. As Kotlin Multiplatform Mobile is becoming new silver bullet for writing cross-platform logic for both Android and iOS, many of us will face the same problem in a near future. Network requests in multiplatform projects (KMP) are done using Ktor library, therefor below I’ll show the implementation of two common tasks: adding header parameters to requests and handling custom errors from server, using Ktor’s HttpResponseValidator (OkHttp Interceptor alternative).

If you’re not familiar with networking using Ktor yet, I recommend you to complete this hand-on guide before going further. https://play.kotlinlang.org/hands-on/Networking%20and%20Data%20Storage%20with%20Kotlin%20Multiplatfrom%20Mobile/01_Introduction

Adding header params to requests is pretty straightforward, you just have to set up the HttpClient, you already should be familiar with that:

val httpClient = HttpClient {
install(JsonFeature) {
val json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }
serializer = KotlinxSerializer(json)
}
}

Here we’re setting up the basic client, which works with Json and can serialize it using KotlinxSerializer.

2. And then you can add interceptor for headers. To do this just add another install block inside your HttpClient body and add headers to HttpRequestBuilder:

install(DefaultRequest) {
apply {
headers.apply {
append("Accept", "application/json")
append("Authorization", "Bearer $token")
}
}
}

Api class so far: https://gist.github.com/yusufabd/9c1ff7d19805a788d6634388db6aff1f

Handling errors is a bit non-trivial task, especially if you have custom payload for failed requests. For example, user might be using no longer supported version of the app and we should force him to update the app. To do that remote server might return 400 Bad Request Error containing such payload:

{
"error": 777,
"data": {
"message": "The current version of the App is no longer supported."
}
}

Therefore when request is not successful, we have to check its payload and throw specific exception. From the first glance you might think that it is obvious how to implement it, we’ve already written code that adds headers to each request before sending it. But to intercept response we can’t do the same, in Ktor this is a bit tricky. I spent couple of hourse digging into forums and bug tickets of Ktor to find the right solution.

For intercepting the response and validating it, HttpResponseValidator should be used, just add it inside HttpClient’s body:

HttpResponseValidator {
validateResponse { response ->
val statusCode = response.status.value
when (statusCode) {
in 300..399 -> throw RedirectResponseException(response)
in 400..499 -> throw ClientRequestException(response)
in 500..599 -> throw ServerResponseException(response)
}

if (statusCode >= 600) {
throw ResponseException(response)
}
}
}

validateResponse block here was provided by official documentation of Ktor. As you can see, here several types of ResponseException are thrown. We can’t directly handle the response and check its payload here. For some reason is stops working correctly if you change it, therefore I’d recommend you to just copy past this block and remain it as it is.
All these ResponseExceptions can be handled in handleResponseException (duh) block, just add it after validateResponse:

handleResponseException { cause ->
val responseException = cause as? ResponseException ?: return@handleResponseException
val response = responseException.response
val bytes = response.receive<ByteArray>()
val string = bytes.decodeToString()
val errorResponse = Json.decodeFromString<ErrorResponse>(string)
if (errorResponse.errorCode == 777) {
throw OldVersionNotSupportedException(
errorResponse.errorCode,
errorResponse.data
)
} else {
throw SomeException(
errorResponse.errorCode,
errorResponse.data
)
}
}

Here we’re getting response from exception, receive its ByteArray and decode it to String. After that you can either serialize it or even parse json manually here (please don’t do that 🙂). Finally, we can throw custom exception based on payload details.

if (errorResponse.errorCode == 777) {
throw OldVersionNotSupportedException(
errorResponse.errorCode,
errorResponse.data
)
} else {
throw SomeException(
errorResponse.errorCode,
errorResponse.data
)
}

23 y.o. android engineer from Uzbekistan