Search code examples
androidretrofitstack-overflowinterceptorkoin

Method threw 'java.lang.StackOverflowError' exception in android when inject UseCase with koin


when I try to inject this line in my repositoryImpl in domain package:

 private val userUseCase: UserUseCase by inject()

I got this error:

java.lang.StackOverflowError: stack size 1040KB

my package structure is: - data -> for retrofit, room, and ... - domain -> repository implement here and make connection between data and presentation - presentation -> for UI

I think with because of bad implement in DI and Provider of Koin. and this is my network-provider and network-module:

NetworkProvider:

    /**
 * provide HttpLoggingInterceptor for dependency injection with *Koin*
 *
 * @return the HttpLoggingInterceptor object <HttpLoggingInterceptor>
 *
 * @see HttpLoggingInterceptor
 */
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
    val logger = HttpLoggingInterceptor()
    logger.level = HttpLoggingInterceptor.Level.BASIC
    return logger
}


/**
 * provide OkHttpClient for dependency injection with *Koin*
 *
 * @param loggingInterceptor: a HttpLoggingInterceptor object, injected
 * @param cache: a Cache object, injected
 * @param hostnameVerifier: a HostnameVerifier object, injected
 *
 * @return the OkHttpClient built object <OkHttpClient>
 *
 * @see HttpLoggingInterceptor
 * @see Cache
 * @see HostnameVerifier
 */
fun provideOkHttpClient(
    loggingInterceptor: HttpLoggingInterceptor,
    cache: Cache,
    hostnameVerifier: HostnameVerifier
): OkHttpClient {

    val clientBuilder = OkHttpClient()
        .newBuilder()
        .followRedirects(true)
        .followSslRedirects(true)
        .retryOnConnectionFailure(true)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .hostnameVerifier(hostnameVerifier)
        .addInterceptor(loggingInterceptor)
        .cache(cache)

    return clientBuilder.build()
}


/**
 * provide provideOkHttpClientForRefresh for dependency injection with *Koin*
 *
 * @param loggingInterceptor: a HttpLoggingInterceptor object, injected
 * @param cache: a Cache object, injected
 * @param hostnameVerifier: a HostnameVerifier object, injected
 *
 * @return the OkHttpClient built object <OkHttpClient>
 *
 * @see HttpLoggingInterceptor
 * @see Cache
 * @see HostnameVerifier
 */
fun provideOkHttpClientForRefresh(
    loggingInterceptor: HttpLoggingInterceptor,
    refreshTokenInterceptor: RefreshTokenInterceptor,
    cache: Cache,
    hostnameVerifier: HostnameVerifier
): OkHttpClient {

    val clientBuilder = OkHttpClient()
        .newBuilder()
        .followRedirects(true)
        .followSslRedirects(true)
        .retryOnConnectionFailure(true)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .hostnameVerifier(hostnameVerifier)
        .addInterceptor(loggingInterceptor)
        .addInterceptor(refreshTokenInterceptor)
        .cache(cache)

    return clientBuilder.build()
}


/**
 * provide OkHttpClient for dependency injection with *Koin*
 *
 * @param loggingInterceptor: a HttpLoggingInterceptor object, injected
 * @param accessTokenInterceptor: a AccessTokenInterceptor object, injected
 * @param accessTokenAuthenticator: a AccessTokenAuthenticator object, injected
 * @param cache: a Cache object, injected
 * @param hostnameVerifier: a HostnameVerifier object, injected
 *
 * @return the OkHttpClient built object <OkHttpClient>
 *
 * @see HttpLoggingInterceptor
 * @see AccessTokenInterceptor
 * @see AccessTokenAuthenticator
 * @see Cache
 * @see HostnameVerifier
 */

fun provideOkHttpClientForAuth(
    loggingInterceptor: HttpLoggingInterceptor,
    accessTokenInterceptor: AccessTokenInterceptor,
    accessTokenAuthenticator: AccessTokenAuthenticator,
    cache: Cache,
    hostnameVerifier: HostnameVerifier
): OkHttpClient {

    val clientBuilder = OkHttpClient()
        .newBuilder()
        .followRedirects(true)
        .followSslRedirects(true)
        .retryOnConnectionFailure(true)
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .hostnameVerifier(hostnameVerifier)
        .authenticator(accessTokenAuthenticator)
        .addInterceptor(accessTokenInterceptor)
        .addInterceptor(loggingInterceptor)
        .cache(cache)

    return clientBuilder.build()
}


/**
 * provide Cache for dependency injection with *Koin*
 *
 * @param cacheDirection: a CacheDirection object, injected
 *
 * @return the Cache object <Cache>
 *
 *  @see CacheDirection
 *  @see Cache
 */
fun provideCache(cacheDirection: CacheDirection): Cache {

    val cacheFile = createDefaultCacheDir(cacheDirection.path, "api_cache")

    return Cache(cacheFile, calculateDiskCacheSize(cacheFile))
}


/**
 * provide HostnameVerifier for dependency injection with *Koin*
 *
 * @return the HostnameVerifier object <HostnameVerifier>
 *
 *  @see HostnameVerifier
 */
fun provideHostnameVerifier(): HostnameVerifier {

    return HostnameVerifier { hostname, _ ->

        return@HostnameVerifier BuildConfig.API_URL_V1.contains(hostname)
    }
}

/**
 * provide Retrofit for dependency injection with *Koin*
 *
 * @param okHttpClient: a OkHttpClient object, injected
 *
 * @return the Retrofit built object <Retrofit>
 *
 * @see OkHttpClient
 * @see Retrofit
 */
fun provideRetrofitForAuth(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder().baseUrl(BuildConfig.API_URL_V1).client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create()).build()
}

/**
 * provide Retrofit for dependency injection with *Koin*
 *
 * @param okHttpClient: a OkHttpClient object, injected
 *
 * @return the Retrofit built object <Retrofit>
 *
 * @see OkHttpClient
 * @see Retrofit
 */
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder().baseUrl(BuildConfig.API_URL_V1).client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create()).build()
}


/**
 * provide Retrofit for dependency injection with *Koin*
 *
 * @param okHttpClient: a OkHttpClient object, injected
 *
 * @return the Retrofit built object <Retrofit>
 *
 * @see OkHttpClient
 * @see Retrofit
 */
fun provideOkHttpClientForRefresh(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder().baseUrl(BuildConfig.API_URL_V1).client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create()).build()
}


/**
 * provide AuthApi for dependency injection with *Koin*
 *
 * @param retrofit: a Retrofit object, injected
 *
 * @return object created retrofit from retrofit's interface <AuthApi>
 *
 * @see Retrofit
 * @see AuthApi
 */
fun provideAuthApi(retrofit: Retrofit): AuthApi = retrofit.create(AuthApi::class.java)


/**
 * provide CheckTokenApi for dependency injection with *Koin*
 *
 * @param retrofit: a Retrofit object, injected
 *
 * @return object created retrofit from retrofit's interface <AuthApi>
 *
 * @see Retrofit
 * @see CheckTokenApi
 */
fun provideCheckTokenApi(retrofit: Retrofit): CheckTokenApi =
    retrofit.create(CheckTokenApi::class.java)


/**
 * provide AuthApi for dependency injection with *Koin*
 *
 * @param retrofit: a Retrofit object, injected
 *
 * @return object created retrofit from retrofit's interface <AuthApi>
 *
 * @see Retrofit
 * @see AuthApi
 */
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)

NetworkModule:

    /**
 * Create network provider module for dependency injection with *Koin*
 *
 * @see provideRetrofit
 * @see provideOkHttpClient
 * @see provideLoggingInterceptor
 * @see provideAuthApi
 * @see ResponseHandler
 * @see AccessTokenInterceptor
 * @see AccessTokenAuthenticator
 */


val networkModule = module {
    factory { provideLoggingInterceptor() }
    factory { provideCache(get()) }
    factory { provideHostnameVerifier() }
    factory { ResponseHandler() }
    factory { AccessTokenInterceptor(get()) }
    factory { AccessTokenAuthenticator(get(), get()) }
    factory { RefreshTokenInterceptor(get(), get()) }

    factory(named("allRequestOkHttpClient")) {
        provideOkHttpClient(
            get(),
            get(),
            get()
        )
    }


    factory(named("refreshTokenRequestOkHttpClient")) {
        provideOkHttpClientForRefresh(
            get(),
            get(),
            get(),
            get()
        )
    }


    factory(named("authRequestOkHttpClient")) {
        provideOkHttpClientForAuth(
            get(), get(), get(), get(), get()
        )
    }

    single(named("allRequestRetrofit")) { provideRetrofit(get(named("allRequestOkHttpClient"))) }
    single(named("refreshTokenRequestRetrofit")) { provideOkHttpClientForRefresh(get(named("refreshTokenRequestOkHttpClient"))) }
    single(named("authRequestRetrofit")) { provideRetrofitForAuth(get(named("authRequestOkHttpClient"))) }


    factory { provideAuthApi(get(named("allRequestRetrofit"))) }
    factory { provideCheckTokenApi(get(named("refreshTokenRequestRetrofit"))) }
    factory { provideUserApi(get(named("authRequestRetrofit"))) }

}

AccessTokenAuthenticator:

  /**
 * a Authenticator class for add *Authorization* header into *okhttp* request
 * implement from Authenticator (okhttp3.Authenticator) and KoinComponent (org.koin.core.KoinComponent)
 */
class AccessTokenAuthenticator(
    private val checkTokenApi: CheckTokenApi,
    private val userPreferences: UserPreferences
) : Authenticator {

    /**
     * override function for handle add *Authorization* into *okhttp* request
     *
     * @param route
     * @param response
     *
     * @return Request
     */
    @Nullable
    override fun authenticate(route: Route?, response: Response): Request? {

        synchronized(this) {

            val newAccessToken =
                checkTokenApi.checkToken().execute().body()

            return if (userPreferences.token != newAccessToken?.apiObjects?.user?.token) {
                if (newAccessToken?.apiObjects?.user?.token?.isNotEmpty() == true) {
                    userPreferences.token = newAccessToken.apiObjects.user.token
                }
                newRequestWithAccessToken(
                    response.request(),
                    newAccessToken?.apiObjects?.user?.token ?: ""
                )
            } else {

                newRequestWithAccessToken(response.request(), userPreferences.token)
            }

        }
    }


    /**
     * create request with custom header(Device-Id, Device-Token)
     *
     * @param request request for add header <Request>
     * @param accessToken for add Uer-Token header provided from server <String>
     *
     * @return a Request with custom header(auth)
     */
    private fun newRequestWithAccessToken(
        request: Request,
        newToken: String
    ): Request {

        val req = request.newBuilder()

        if (userPreferences.isLogin()) {

            req.header("auth", newToken)
        }

        return req.build()
    }

AccessTokenInterceptor:

    /**
 * a Interceptor class for handel or add *Authorization* header into *okhttp* request after get response
 * implement from Interceptor (okhttp3.Interceptor) and KoinComponent (org.koin.core.KoinComponent)
 */
class AccessTokenInterceptor(
    private val userPreferences: UserPreferences
) : Interceptor {

    /**
     * override function for handle or add *Authorization* into *okhttp* request after get response
     *
     * @param chain intercept's chain <Interceptor.Chain>
     *
     * @return Response
     */
    override fun intercept(chain: Interceptor.Chain): Response {


        /*  if (userPreferences.isLogin()) {
            if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

                response = getToken(chain, userPreferences.token)
            }*/

        return when (userPreferences.isLogin()) {

            true -> {

                val request = newRequestWithAccessToken(chain.request(), userPreferences.token)
                chain.proceed(request)
            }
            else -> {

                val request = newRequestWithoutAccessToken(chain.request())
                chain.proceed(request)
            }
        }

    }


    /**
     * create synchronized Api call for get device token and add into header and add into devicePreferences
     *
     * @param chain intercept's chain <Interceptor.Chain>
     *
     * @return a Response with custom header(Device-Id, Device-Token)
     */
 /*   private fun getToken(chain: Interceptor.Chain, previousToken: String): Response {

        synchronized(this) {

            val refreshTokenWithPreviousToken = chain.request().newBuilder()

            refreshTokenWithPreviousToken.header("auth", previousToken)

            val newAccessToken =
                userApi.checkToken().execute().body()


            if (userPreferences.token != newAccessToken?.apiObjects?.user?.token) {
                if (newAccessToken?.apiObjects?.user?.token?.isNotEmpty() == true) {
                    userPreferences.token = newAccessToken.apiObjects.user.token
                }

            }

            return chain.proceed(
                newRequestWithAccessToken(
                    refreshTokenWithPreviousToken.build(),
                    newAccessToken?.apiObjects?.user?.token ?: ""
                )
            )

        }
    }*/

    /**
     * create request with custom header(Device-Id, Device-Token)
     *
     * @param request request for add header <Request>
     * @param accessToken for add Device-Token header provided from server <String>
     *
     * @return a Request with custom header(Device-Id, Device-Token)
     */
    private fun newRequestWithAccessToken(
        request: Request,
        accessToken: String
    ): Request {

        val req = request.newBuilder()

        if (userPreferences.isLogin()) {

            req.header("auth", accessToken)
        }

        return req.build()
    }


    private fun newRequestWithoutAccessToken(
        request: Request
    ): Request {

        val req = request.newBuilder()

        return req.build()
    }

} 

RefreshTokenInterceptor:

 /**
 * a Interceptor class for handel or add *Authorization* header into *okhttp* request after get response
 * implement from Interceptor (okhttp3.Interceptor) and KoinComponent (org.koin.core.KoinComponent)
 */
class RefreshTokenInterceptor(
    private val checkTokenApi: CheckTokenApi,
    private val userPreferences: UserPreferences
) : Interceptor {

    /**
     * override function for handle or add *Authorization* into *okhttp* request after get response
     *
     * @param chain intercept's chain <Interceptor.Chain>
     *
     * @return Response
     */
    override fun intercept(chain: Interceptor.Chain): Response {

        var response: Response

        response = when (userPreferences.isLogin()) {

            true -> {

                val request = newRequestWithAccessToken(chain.request(), userPreferences.token)
                chain.proceed(request)
            }
            else -> {

                val request = newRequestWithoutAccessToken(chain.request())
                chain.proceed(request)
            }
        }

        if (userPreferences.isLogin()) {
            if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

                response = getToken(chain, userPreferences.token)
            }
        }



        return response
    }


    /**
     * create synchronized Api call for get device token and add into header and add into devicePreferences
     *
     * @param chain intercept's chain <Interceptor.Chain>
     *
     * @return a Response with custom header(Device-Id, Device-Token)
     */
    private fun getToken(chain: Interceptor.Chain, previousToken: String): Response {

        synchronized(this) {

            val refreshTokenWithPreviousToken = chain.request().newBuilder()

            refreshTokenWithPreviousToken.header("auth", previousToken)

            val newAccessToken =
                checkTokenApi.checkToken().execute().body()


            if (userPreferences.token != newAccessToken?.apiObjects?.user?.token) {
                if (newAccessToken?.apiObjects?.user?.token?.isNotEmpty() == true) {
                    userPreferences.token = newAccessToken.apiObjects.user.token
                }

            }

            return chain.proceed(
                newRequestWithAccessToken(
                    refreshTokenWithPreviousToken.build(),
                    newAccessToken?.apiObjects?.user?.token ?: ""
                )
            )

        }
    }

    /**
     * create request with custom header(Device-Id, Device-Token)
     *
     * @param request request for add header <Request>
     * @param accessToken for add Device-Token header provided from server <String>
     *
     * @return a Request with custom header(Device-Id, Device-Token)
     */
    private fun newRequestWithAccessToken(
        request: Request,
        accessToken: String
    ): Request {

        val req = request.newBuilder()

        if (userPreferences.isLogin()) {

            req.header("auth", accessToken)
        }

        return req.build()
    }


    private fun newRequestWithoutAccessToken(
        request: Request
    ): Request {

        val req = request.newBuilder()

        return req.build()
    }

}

I have no idea why this problem occur but this end-point I try to call need auth header and token. but for another end-point without any like login and ... everything for correctly. may please guide my to a found a problem. thank you


Solution

  • I changed this class and everything now work correctly:

    AccessTokenAuthenticator:

    /**
     * a Authenticator class for add *Authorization* header into *okhttp* request
     * implement from Authenticator (okhttp3.Authenticator) and KoinComponent (org.koin.core.KoinComponent)
     */
    class AccessTokenAuthenticator(
        private val checkTokenApi: CheckTokenApi,
        private val userPreferences: UserPreferences
    ) : Authenticator {
    
        /**
         * override function for handle add *Authorization* into *okhttp* request
         *
         * @param route
         * @param response
         *
         * @return Request
         */
        @Nullable
        override fun authenticate(route: Route?, response: Response): Request? {
    
            if (response.code() == 401) {
                try {
                    val sendCall = checkTokenApi.refreshToken()
                    val refreshResult = sendCall.execute()
                    if (refreshResult.isSuccessful) {
                        //save Token
                        userPreferences.token = refreshResult.body()?.apiObjects?.user?.token ?: ""
    
                        //Replace Token
                        return response.request().newBuilder()
                            .header(
                                "auth",
                                refreshResult.body()?.apiObjects?.user?.token ?: ""
                            )
                            .build()
                    }
                } catch (ex: Exception) {
                    // todo :: handle error
                    println(ex)
                }
    
            }
            return null
        }
    }
    

    AccessTokenInterceptor:

    /**
     * a Interceptor class for handel or add *Authorization* header into *okhttp* request after get response
     * implement from Interceptor (okhttp3.Interceptor) and KoinComponent (org.koin.core.KoinComponent)
     */
    class AccessTokenInterceptor(
        private val userPreferences: UserPreferences
    ) : Interceptor {
    
        /**
         * override function for handle or add *Authorization* into *okhttp* request after get response
         *
         * @param chain intercept's chain <Interceptor.Chain>
         *
         * @return Response
         */
        override fun intercept(chain: Interceptor.Chain): Response {
    
            val request = chain.request()
            val newRequest: Request
    
            newRequest = request.newBuilder()
                .addHeader("auth", userPreferences.token)
                .build()
            return chain.proceed(newRequest)
        }
    }
    

    RefreshTokenInterceptor:

    /**
     * a Interceptor class for handel or add *Authorization* header into *okhttp* request after get response
     * implement from Interceptor (okhttp3.Interceptor) and KoinComponent (org.koin.core.KoinComponent)
     */
    class RefreshTokenInterceptor(
        private val userPreferences: UserPreferences
    ) : Interceptor {
    
    
        /**
         * override function for handle or add *Authorization* into *okhttp* request after get response
         *
         * @param chain intercept's chain <Interceptor.Chain>
         *
         * @return Response
         */
        override fun intercept(chain: Interceptor.Chain): Response {
    
            val request = chain.request()
            val newRequest: Request
    
            newRequest = request.newBuilder()
                .addHeader("auth", userPreferences.token)
                .build()
            return chain.proceed(newRequest)
        }
    }
    

    and I think we can just remove RefreshTokenInterceptor and just use AccessTokenInterceptor. I hope this helps someone else.