I have two HttpClient instances inside my NetworkM...
# multiplatform
a
I have two HttpClient instances inside my NetworkModule. One is used for user authentication during login and does not require an authorization bearer access token. The second one is used for authorized API calls. These two clients are singleton instances within the application and are used in the service layer to make API calls. After a successful login, I retrieve the refresh and access tokens and store them in the shared preferences of my KMM project [com.russhwolf.settings]. This setup works fine for API calls with the bearer token. When the access token becomes invalid, I make an API call to refresh the token. The tokens are updated, and everything works fine so far. The problem arises when the refresh token also becomes invalid, causing the user to be signed out of the app. If the user logs in again, the tokens are not updated in the HttpClient. Here is my code snippet :
Copy code
class TokenRepository(private val settings: Settings) {

    private val accessTokenKey = "AccessToken"
    private val refreshTokenKey = "RefreshToken"
    private val userIdKey = "UserId"

    private fun fetchRefreshToken() = settings.getStringOrNull(refreshTokenKey)

    fun saveTokens(authResponse: AuthResponse) {
        settings.putString(accessTokenKey, authResponse.accessToken)
        settings.putString(refreshTokenKey, authResponse.refreshToken)
        settings.putInt(userIdKey, authResponse.userId)
    }

    fun saveTokens(bearerTokens: BearerTokens) {
        settings.putString(accessTokenKey, bearerTokens.accessToken)
        settings.putString(refreshTokenKey, bearerTokens.refreshToken)
    }

    fun fetchUserId() = settings.getInt(userIdKey, 0)

    fun fetchBearerTokens(): BearerTokens? {
        val accessToken = settings.getStringOrNull(accessTokenKey)
        val refreshToken = settings.getStringOrNull(refreshTokenKey)

        return if (accessToken != null && refreshToken != null) {
            BearerTokens(accessToken, refreshToken)
        } else null
    }

    fun destroyTokens() = settings.clear()

    suspend fun refreshToken(): BearerTokensDTO =
        HttpClient {
            defaultRequest {
                url {
                    protocol = URLProtocol.HTTP
                    host = OpMConfig.BASE_URL
                    path(ApiVersion.Version.versionName)
                }
                header(HttpHeaders.ContentType, ContentType.Application.Json)
            }
            install(Logging) {
                logger = HttpClientLogger
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(NetworkModule.nonStrictJson)
            }
        }.post(ApiEndpoint.RefreshToken.path) {
            setBody(mapOf(ApiField.RefreshToken to fetchRefreshToken()))
        }.parse()
}

object NetworkModule : KoinComponent {
    const val BASE_CLIENT = "BaseHttpClient"
    const val AUTH_CLIENT = "AuthHttpClient"

    val nonStrictJson = Json { isLenient = true; ignoreUnknownKeys = true }

    private val tokenRepository: TokenRepository by inject()

    private val baseHttpClient = HttpClient {
        defaultRequest {
            url {
                protocol = URLProtocol.HTTP
                host = BASE_URL
                path(Version.versionName)
            }
            header(HttpHeaders.ContentType, ContentType.Application.Json)
        }
        install(Logging) {
            logger = HttpClientLogger
            level = LogLevel.ALL
        }
        install(ContentNegotiation) {
            json(nonStrictJson)
        }
    }


    private val authHttpClient = baseHttpClient.config {
        install(Auth) {
            bearer {
                sendWithoutRequest { true }
                loadTokens { tokenRepository.fetchBearerTokens() }
                refreshTokens {
                    tokenRepository.refreshToken().toBearerTokens().also {
                        tokenRepository.saveTokens(it)
                    }
                }
            }
        }
    }

    val networkClient = module {
        single(named(BASE_CLIENT)) { baseHttpClient }
        single(named(AUTH_CLIENT)) { authHttpClient }
    }
}
🧵 2
s
authClient.config {}
actually creates and returns a new instance and configures that one. The old instance stays as-is. So in
updateAuthClientHeader
you would have to
authClient = authClient.config { .. }.
I just scrolled through the code, but I'm guessing that's the problem. However, the OAuth plugin can attach the header all by itself. I think you can just remove the
updateAuthClientHeader
function and add this to the initial config
bearer { ...; sendWithoutRequest { true } }
This sendWithoutRequest will tell the client to attach the bearer header to all requests (which is fine since it's your authClient). If you don't set it, it will always try the request and only re-try it with a bearer token if the server responds 401.
a
@s3rius I made changes to the code, but it is not functioning as expected. I don't think it is meant for sendWithoutRequest. Thank you for your response. I have updated my code snippets, but unfortunately, it is still not working. Another point to note is that it seems to work fine when I restart the application.
j
FYI @Aslam Hossin next time please try to put the code snippet in a thread reply to your question, so people reading along don't need to scroll past this big chunk of code 🙂 It also makes it easier to navigate between your question and the answers when the original post fits on the screen 😉
a
@Jacob Ras Thanks for your input. I'll make sure to follow that practice in the future.