Hi everyone, I'm using the Ktor client on Android....
# ktor
v
Hi everyone, I'm using the Ktor client on Android. Whenever I receive a 401 response, the request is triggered again, resulting in two API calls. I’d like to avoid making the call twice.
Copy code
class KtorHttpClientFactory(private val tokenDataSource: TokenDataSource) {

    companion object {
        private const val HOST_URL = "<http://api.vivek.com/dev/v1|api.vivek.com/dev/v1>"
    }

    fun build(): HttpClient {
        return HttpClient(OkHttp) {
            expectSuccess = true
            install(Logging) {
                logger = Logger.ANDROID
                level = LogLevel.ALL
            }

            defaultRequest {
                host = HOST_URL
                url { protocol = URLProtocol.HTTPS }
                header(HttpHeaders.ContentType, ContentType.Application.Json)
                header("client-id", "349587438548375838957893475")
            }

            install(ContentNegotiation) {
                json(
                    Json {
                        prettyPrint = true
                        isLenient = true
                        ignoreUnknownKeys = true
                    }
                )
            }

            install(ResponseObserver) {
                onResponse { response ->
                    Log.d("HTTP status:", "${response.status.value}")
                }
            }
            install(HttpRequestRetry) {
                maxRetries = 0
            }
            configureResponseValidation()

            install(Auth) {
                bearer {
                    // 1) Load tokens from local storage before each request
                    loadTokens {
                        val accessToken = tokenDataSource.getAccessToken().first()
                        val refreshToken = tokenDataSource.getRefreshToken().first()
                        Log.e("KtorHttpClientFactory", "addTokenInterceptor: $accessToken")
                        // Load tokens from a local storage and return them as the 'BearerTokens' instance
                        BearerTokens(accessToken.orEmpty(), refreshToken.orEmpty())
                    }

                    // 2) If 401 is encountered, Ktor calls refreshTokens()
                    refreshTokens { // this: RefreshTokensParams
                        val oldToken = tokenDataSource.getAccessToken().first()

                        val response = <http://client.post|client.post> {
                            url("/refresh")
                            setBody(oldToken)
                        }

                        if (response.status == HttpStatusCode.Unauthorized) {
                            null
                        } else {
                            val authResponse = response.body<AuthResponse>()
                            val refreshToken = authResponse.refreshToken
                            val accessToken = authResponse.accessToken
                            tokenDataSource.storeTokens(refreshToken, accessToken)
                            BearerTokens(refreshToken, accessToken)
                        }
                    }

                    // 3) Send Request without Bearer
                    sendWithoutRequest { request ->
                        request.url.host.contains("/login")
                    }
                }
            }
        }
    }

    private fun HttpClientConfig<OkHttpConfig>.configureResponseValidation() {

        HttpResponseValidator {

            handleResponseExceptionWithRequest { exception, _ ->
                when (exception) {
                    is ResponseException -> {
                        val exceptionResponse = exception.response
                        val statusCode = exceptionResponse.status
                        val (value, description) = HttpStatusCode.fromValue(statusCode.value)
                        val travelException =
                            Json.decodeFromString<TravelException>(exceptionResponse.bodyAsText())
                        throw TravelException(
                            value,
                            description,
                            travelException.serverErrorCode,
                            travelException.serverMessage
                        )
                    }

                    else -> throw TravelException(null, null, serverMessage = exception.message)
                }
            }
        }
    }
}
c
Normally auth is challenge (401) and response (send creds). You can enable the auth creds to be sent initially: https://ktor.io/docs/client-basic-auth.html
m
I think using
Copy code
markAsRefreshTokenRequest()
might solve your problem. Here's an example:
Copy code
val refreshTokenInfo: RefreshTokenResponse = <http://client.post|client.post>(
    "<https://abc.com/api/auth/refresh>"
)
{
    markAsRefreshTokenRequest()
    contentType(ContentType.Application.Json)
    setBody(RefreshTokenRequest(refreshToken!!))
}
k
Thanks Chris for the feedback, what do you mean by auth creds? Do you mean by username and password of login api ?
Thanks Mohsen for the feedback, why do we need this new markAs.. function?
m
By “auth creds,” The
markAsRefreshTokenRequest()
function helps to distinguish the token refresh operation from other API calls.
k
Thanks for clarifying
Another thing, I don't want to send an authorization token in every call like register, login etc. so how can I achieve this ?
s
See the conversation in this thread last week: https://kotlinlang.slack.com/archives/C0A974TJ9/p1734534851284049
Basically, you can either: • Define two HttpClients, one authenticated and one unauthenticated • OR Use conditional logic in the auth block to only authorize calls to certain endpoints
k
Thank you, it works