Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Coroutine runblock with the Authenticator to handle 401 response from retrofit

I am trying to use the Authenticator to handle 401 response. What I have done is

fun provideAccessTokenAuthenticator(
    mainApiServiceHolder: MainApiServiceHolder,
    preferences: SharedPreferences
) = object : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val accessToken = preferences.getString(ACCESS_TOKEN, null)
        if (!isRequestWithAccessToken(response) || accessToken == null) {
            return null
        }
        synchronized(this) {
            val newAccessToken = preferences.getString(ACCESS_TOKEN, null)!!
            // Access token is refreshed in another thread.
            if (accessToken != newAccessToken) {
                return newRequestWithAccessToken(response.request, newAccessToken)
            }

            // Need to refresh an access token
            val refreshTokenResponse = runBlocking {
                Log.d("zzzzzzzzzz", "refresh token is running")
                mainApiServiceHolder.mainApiService?.refreshToken(
                    "refresh_token",
                    preferences.getString(REFRESH_TOKEN, null)!!,
                    AuthRepository.CLIENT_ID,
                    AuthRepository.CLIENT_SECRET
                )
            }
            Log.d("zzzzzzzzzz", refreshTokenResponse?.body()?.access_token!!)
            return if (refreshTokenResponse?.isSuccessful!!) {
                Log.d("zzzzzzzzzz", "refresh token is successful")
                newRequestWithAccessToken(
                    response.request,
                    refreshTokenResponse.body()?.access_token!!
                )
            } else {
                Log.d("zzzzzzzzzz", "refresh token is unsuccessful")
                response.request.newBuilder().header("Content-Type", "application/json").build()
            }
        }
    }

Now, it gets called when there is a 401 response. The refresh token call is also fired (from Log). However, it never gets the result in the refreshTokenResponse and nothing happens after that. I think its a wrong way of using runBlock. The api is

@FormUrlEncoded
@POST("/api/auth/token/")
suspend fun refreshToken(
    @Field("grant_type") grant_type: String,
    @Field("refresh_token") refresh_token: String,
    @Field("client_id") client_id: String,
    @Field("client_secret") client_secret: String
): Response<LoginResponse>

Any help would be really appreciated. Thanks

like image 490
sadat Avatar asked Jul 17 '20 08:07

sadat


1 Answers

In the Retrofit API, consider replacing your async runBlocking{} suspend fun with a synchronous Call. I had the most luck avoiding the use of coroutines inside the Authenticator.

I was having the same problem. The token request went straight into a black hole. The app froze. The request was never seen again. No error, no nothing.

But everywhere else in the app, the suspend fun came back just fine. From ViewModels, from WorkManager, it worked every time. But from the Authenticator, never. What was wrong with the Authenticator? What was special about the Authenticator that made it act this way?

Then I replaced the runBlocking{} coroutine with a straightforward Call. This time, the request came back and the token arrived without a fuss.

The way I got the API to work looked like this:

@FormUrlEncoded
@POST("token")
fun refreshTokenSync(
    @Field("refresh_token") refreshToken: String,
): Call<RefreshMyTokenResponse>

Then, in the Authenticator:

 val call = API.refreshTokenSync(refreshToken)
 val response = call.execute().body()

I hope this helps someone else who ran into the same issue. You may receive a warning from Android Studio that this is an inappropriate blocking call. Ignore it.

like image 110
John Gorenfeld Avatar answered Nov 05 '22 01:11

John Gorenfeld