How to check token expiration at interceptors android retrofit? - android

I would like to handle token expiration by myself and send request for new tokens. I have such condition:
sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60
This condition checks when my token will become expired and I have to send a new request from interceptor. I saw this question also. I have created such interceptor:
class RefreshTokens(cont: Context) : Interceptor{
val context = cont
override fun intercept(chain: Interceptor.Chain): Response {
var tokenIsUpToDate = false
val sp = context.getSharedPreferences("app_data", 0)
if (sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60) {
Singleton.apiService(context).getNewToken(ReqAccessToken(context.getSharedPreferences("app_data", 0).getString("refresh_token", ""))).enqueue(object : Callback<ResNewTokens>, retrofit2.Callback<ResNewTokens> {
override fun onResponse(call: Call<ResNewTokens>, response: retrofit2.Response<ResNewTokens>) {
if (response.isSuccessful) {
tokenIsUpToDate = true
}
}
override fun onFailure(call: Call<ResNewTokens>, t: Throwable) {
}
})
return if (tokenIsUpToDate) {
chain.proceed(chain.request())
} else {
chain.proceed(chain.request())
}
} else {
val response = chain.proceed(chain.request())
when (response.code) {
401->{
chain.request().url
response.request.newBuilder()
.header("Authorization", "Bearer " + context.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
.build()
}
500 -> {
Toast.makeText(context, context.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
}
}
return response
}
}
}
I can't imagine how to add return condition to my code. I know about Authentificator but when I use it I send one more request which response gives me 401 error for token updating. When I use Authentificator I send such requests:
Request with old access_token -> 401 error
Request for the new tokens -> 200 OK
Request with new access_token -> 200 OK
So I would like to remove 1 request which will give error and send request for a new tokens. But I have to problems:
I don't know how to fix my interceptor for solving this task
I don't know how to repeat request which I was going to make like in Authentificator
Maybe someone knows how to solve my problem?

Yes that is too much simple do not take is difficult, I also have same issue but i solve like this
So When the token is expred so the Retrofit give the
Error Code = 401
So you need to save the data of user Using sharedPref the userEmail or userName as well as userPassword So
When the user get token exipre message or error code 401 then you need to call a method to login the user again to show anything to the user using useremail and userpassword and then a fresh token generated then send that generated Token to the server and it will work in this case
I hope that will help

I would like to share my own solution which works good as I see:
class AuthToken(context: Context) : Interceptor {
var cont = context
override fun intercept(chain: Interceptor.Chain): Response {
val sp = cont.getSharedPreferences("app_data", 0)
if (sp!!.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60 && !sp.getString("refresh_token", "")!!.isBlank()) updateAccessToken(cont)
val initialRequest = if (sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60 && !sp.getString("refresh_token", "")!!.isBlank()) {
updateAccessToken(cont)
requestBuilder(chain)
} else {
requestBuilder(chain)
}
val initialResponse = chain.proceed(initialRequest)
return if (initialResponse.code == 401 && !sp.getString("refresh_token", "").isNullOrBlank() && sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60) {
updateAccessToken(cont)
initialResponse.close()
val authorizedRequest = initialRequest
.newBuilder()
.addHeader("Content-type:", "application/json")
.addHeader("Authorization", "Bearer " + cont.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
.build()
chain.proceed(authorizedRequest)
} else {
val errorBody = initialResponse.message
when {
}
if (initialResponse.code == 500) {
val thread = object : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
Looper.loop()
}
}
thread.start()
}
initialResponse
}
}
private fun updateAccessToken(context: Context) {
val sp = context.getSharedPreferences("app_data", 0)
synchronized(this) {
val tokensCall = accessTokenApi()
.getNewToken(ReqAccessToken(context.getSharedPreferences("app_data", 0).getString("refresh_token", "")!!))
.execute()
if (tokensCall.isSuccessful) {
val responseBody = tokensCall.body()
val editor = sp.edit()
val localTime = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH).parse(tokensCall.headers()["Date"]!!)
Singleton.setServerTime(localTime!!.time / 1000, context)
editor.putString("access_token", Objects.requireNonNull<ResNewTokens>(responseBody).access_token).apply()
editor.putString("refresh_token", Objects.requireNonNull<ResNewTokens>(responseBody).refresh_token).apply()
editor.putLong("expires_in", responseBody!!.expires_in!!).apply()
} else {
when (tokensCall.code()) {
500 -> {
val thread = object : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
Looper.loop()
}
}
thread.start()
}
401 -> {
Singleton.logOut(context)
}
}
}
}
}
private fun requestBuilder(chain: Interceptor.Chain): Request {
return chain.request()
.newBuilder()
.header("Content-type:", "application/json")
.header("Authorization", "Bearer " + cont.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
.build()
}
private fun accessTokenApi(): APIService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val dispatcher = Dispatcher()
dispatcher.maxRequests = 1
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(100, TimeUnit.SECONDS)
.dispatcher(dispatcher)
.readTimeout(100, TimeUnit.SECONDS).build()
client.dispatcher.cancelAll()
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(APIService::class.java)
}
}
In general as I see I send request for token refreshing before send request with expired access_token. Maybe someone will have some suggestions or improvements for my solution :)

Related

Refresh access token using authenticatior in retrofit android

How can I refresh my token using authenticator? I need the refresh token method to return the token or null when I get 401 in my api call.
class SupportInterceptor() : Interceptor, Authenticator {
/**
* Interceptor class for setting of the headers for every request
*/
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("app-id", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
?.build()
return chain.proceed(request)
}
/**
* Returns a request that includes a credential to satisfy an authentication challenge in
* [response]. Returns null if the challenge cannot be satisfied.
*
* The route is best effort, it currently may not always be provided even when logically
* available. It may also not be provided when an authenticator is re-used manually in an
* application interceptor, such as when implementing client-specific retries.
*/
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
when (val tokenResponse = refreshToken()) {
is Success -> {
// userPreferences.saveAccessTokens(
// tokenResponse.value.access_token!!,
// tokenResponse.value.refresh_token!!
// )
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse.value.access_token}")
.build()
}
else -> null
}
}
// requestAvailable = response?.request?.newBuilder()
//// ?.addHeader("Authorization", "Bearer $token")
// ?.build()
// return requestAvailable
} catch (ex: Exception) {
}
return requestAvailable
}
suspend fun refreshToken(): Either<Failure, String?> {
return withContext(Dispatchers.IO) {
try {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken = sharedPref.getString(MyConstants.KEY_REFRESH_TOKEN, "")
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api: TokenRefreshApi = retrofit.create(TokenRefreshApi::class.java)
val response = api.refreshAccessToken(refreshToken).execute()
// val call: Call<LogIn> = api.refreshAccessToken(refreshToken)
when (response.isSuccessful) {
false -> Either.Left(response.errorResponse())
true -> {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
MyConstants.KEY_ACCESS_TOKEN,
response.body()!!.access_token
)
editor.putString(
MyConstants.KEY_REFRESH_TOKEN,
response.body()!!.refresh_token
)
editor!!.apply()
response.body()!!.access_token
}
}
} catch (e: Exception) {
Timber.e("searchTasks: $e")
Either.Left(Failure.UnknownError)
}
}
}
}
I would first clarify the industry standard behavior for reliable clients:
Client tries an API request with an access token
If client receives a 401 it attempts to silently refresh the access token and retry the API request with the new token
If there are technical problems avoid redirecting the user to re-authenticate
Here is some plain Kotlin code of mine that does this.
Looks to me like Retrofit's Authenticator interface makes this easier, and will do the retry for you. Your code looks mostly correct and similar to mine but without the manual checks for 401s:
You need to test 401s though, and one way to do this is to add arbitrary characters to an access token during development, to simulate expiry
In case someone is having difficulty same like me.
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
val tokenResponse = getNewToken()
if (!tokenResponse.isNullOrEmpty()) {
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse}")
.build()
} else {
null
}
}
} catch (ex: Exception) {
}
return requestAvailable
}
private fun getNewToken(): String? {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken: String = sharedPref.getString("refresh_token", "").orEmpty()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_API_URL_CONSUMER)
.addConverterFactory(GsonConverterFactory.create())
.build()
val hashMap = HashMap<String, String>()
hashMap.put("refresh_token", refreshToken)
val call = retrofit.create(TokenRefreshApi::class.java).refreshAccessToken(hashMap)
val authTokenResponse = call?.execute()?.body()
if (authTokenResponse != null) {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
"access_token",
authTokenResponse!!.access_token
)
editor.putString(
"refresh_token",
authTokenResponse!!.refresh_token
)
editor!!.apply()
return authTokenResponse!!.access_token
} else {
return null
}
}

Retrofit put method call update the item but give 400 response code in error in android

I am using Retrofit to update some information. The information is updated in the database. But i am getting 400 error code in response. At the same time the API works perfectly in postman.
I have double-checked that I'm sending the required headers and the API token which updated on every login. But I'm get 400 error still, while the information is updated.
this is
You are getting 401 as a status code that means unauthorized token you are passing or something wrong with the auth token
Check auth token you are passing is correct or not or you are passing it or not.
If you are not passing auth token in the api header then please pass it will resolve your error
this is api module class
var token = ""
if (prefs.contains(Constants.TOKEN_VALUE)) {
prefs.read(Constants.TOKEN_VALUE)?.let {
token = it
}
}
val httpInterceptor = HttpLoggingInterceptor()
httpInterceptor.level = httpLoggingLevel
val okHttp = OkHttpClient.Builder()
.addInterceptor(httpInterceptor).addInterceptor { chain ->
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("Accept", "application/json")
requestBuilder.addHeader("Cache-Control", "no-cache")
requestBuilder.addHeader("Content-Type", "application/x-www-form-urlencoded")
if (token.isNotEmpty() && Constants.apitokenheader==0) {
requestBuilder.addHeader("Authorization", token)
Log.d("apitoken", "providesBaseApiService: $token")
}
chain.proceed(requestBuilder.build())
}
.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
when(response.code()){
200, 201 -> response
204 -> response.newBuilder().code(200).body(ResponseBody.create(MediaType.get("application/json"), "1")).build()
else -> {
try {
response.body()?.byteStream()?.readBytes()?.toString(Charset.defaultCharset())?.let {
val obj = JSONObject(it)
val opt = obj.optString("message", "An error occurred, Please try again.")
Log.v("error message", opt)
Log.v("error message1", request.url().toString())
Log.v("error message2", response.code().toString())
val link = request.url().uri().toString()
val sub : String = link.substringAfterLast("v1")
Log.v("DripInventory", sub)
Log.v("DripInventory", link.indexOf("v1").let { if (it == -1) null else link.substring(it + 1) })
// link.indexOf("v1").let { if (it == -1) null else link.substring(it + 1) }
//response.newBuilder().code(422).message(opt).build()
response.newBuilder().code(response.code().toInt())
.message("Please contact Drip Inventory." +
"\n " +
"\nResponse Code: ${response.code()}" +
"\n " +
"\nApi Call: $sub")
.build()
}
}catch (e: Exception){
response
}
}
}
/*if (response.code() == 204) {
response.newBuilder().code(200).body(ResponseBody.create(MediaType.get("application/json"), "1")).build()
} else {
response
}*/
}
.hostnameVerifier{hostname, session ->
if (hostname == "dripinventory.com") return#hostnameVerifier true
if (hostname == "invalid.demo.dripinventory.com") return#hostnameVerifier true
Log.v("hostname", hostname)
true
}
.build()
val gson = GsonBuilder()
// Serializers
.registerTypeAdapter(CreateAssetRequest::class.java, CreateAssetRequestSerializer())
.registerTypeAdapter(UpdateAssetRequest::class.java, UpdateAssetRequestSerializer())
// Deserializers
.registerTypeAdapter(AssetRaw::class.java, AssetRawDeserializer())
.create()
val retrofit = Retrofit.Builder()
.client(okHttp)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
return retrofit.create(CcAssetManagerApi::class.java)

Call Api Again After Token Refresh

I am using Authenticator instead of Interceptor to refresh the token. I am able to detect the 401 exception and easily refresh the new token. Everything is working perfectly but the issue is following:
I am unable to call the request again, I do not want the user to hit again to call the offer.
So after execution of the code below I get a new token, it gives me a 401 error message.
My Question is: How can I call the request chain again?
Any advice on the implementation is welcome.
class OffersViewModel
val observable = ApiServiceClient.createApiUsingToken(context).getOffers(
Pref.getString(getApplication(), Pref.CUSTOMER_CODE, "")!!,
Pref.getString(getApplication(), Pref.TOKEN, "")!!
)
compositeDisposable.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
responseModel.statusCode = StatusCode.START
offersRegisteredUserResponseLiveData.postValue(responseModel)
}
.subscribe({ success ->
if (success.errors.isNullOrEmpty()) {
success.statusCode = StatusCode.SUCCESS
} else {
success.statusCode = StatusCode.ERROR
}
offersRegisteredUserResponseLiveData.value = success
}, {
//HERE I GOT 401
Log.d("debug",it.message.toString())
responseModel.statusCode = StatusCode.ERROR
offersRegisteredUserResponseLiveData.value = responseModel
}, { })
)
API Service Class
/*.....Offer Screen...........*/
#GET("offers/xyz/{abc}")
fun getOffers(
#Path("abc") customerCode: String,
#Header("Authorization") authorization: String,
#Header("Content-Type") contentType: String = CONTENT_TYPE
):
Observable<OfferRegisteredUserResponseModel>
ApiClient Class
fun createApiUsingToken(context: Context?): ApiService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(interceptor).connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.authenticator(TokenInterceptor(context)).build()
val retrofit = Retrofit.Builder()
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Constants.BASE_URL)
.build()
var ApiServiceClient=retrofit.create(ApiService::class.java)
return retrofit.create(ApiService::class.java)
}
class TokenInterceptor
var requestAvailable: Request? = null
if (response!!.code() === 401) {
var retrofitResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (retrofitResponse != null) {
val refreshTokenResponse = retrofitResponse!!.body()
val newAccessToken = refreshTokenResponse!!.token
if (newAccessToken != null)
{
Pref.setString(MyApplication.mInstance, Pref.TOKEN, "${refreshTokenResponse.tokenType} ${refreshTokenResponse?.token}")
Pref.setString(MyApplication.mInstance, Pref.TOKEN_EXPIRES_IN, refreshTokenResponse.tokenExpirationTime.toString())
Utils.addTokenExpirationTimeToCurrentTime(MyApplication.mInstance, refreshTokenResponse.tokenExpirationTime?.toInt()!!)
try {
requestAvailable = response?.request()?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("Authorization", "Bearer " + newAccessToken)
?.build()
return requestAvailable
} catch (ex: Exception) {
}
}
} else
return null
}
return requestAvailable
Couple of things i see wrong with this.
First is that even if you "restart" the request with the new token, if you happen to make another request while the "new token" is not saved, that request is also going to fail.
Second is that i don't see that you save the new token anywhere (in SharedPrefs for example for later use).
This is how i would have do it: (preferenceHelper is SharedPrefs)
override fun authenticate(route: Route?, response: Response): Request? {
val HEADER_AUTHORIZATION = "Authorization"
// We need to have a token in order to refresh it.
val token = preferenceHelper.getAccessToken() ?: return null
synchronized(this) {
val newToken = preferenceHelper.getAccessToken() ?: return null
// Check if the request made was previously made as an authenticated request.
if (response.request().header(HEADER_AUTHORIZATION) != null) {
// If the token has changed since the request was made, use the new token.
if (newToken != token) {
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + newToken)
.build()
}
val tokenResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (tokenResponse.isSuccessful) {
val userToken = tokenResponse.body() ?: return null
preferenceHelper.saveAccessToken(userToken.token)
preferenceHelper.saveRefreshToken(userToken.refreshToken)
// Retry the request with the new token.
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + userToken.token)
.build()
} else {
logoutUser()
}
}
}
return null
}

How to create an interceptor for errors to make another call?

I'm getting a 401, because token has expired, so I need to renew the token with another call and then do the call again, is there an easy way instead of doing :
disposable = loginService.login(
UserToLoginRequest(
input_email_login.text.toString(),
input_password_login.text.toString(),
generateRandomDeviceInfo()
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
//It works
},
{ error -> if(error.code == 401) renewAccessToken() }
)
The thing is that I want to do something like this guy : Refreshing Oath token, but if it's possible to call again the same endpoint with the same parameters.
Example
getApple(1) <-- return info of apple id 1
The result is 401 <-- can't do the call without refreshing the accessToken refreshAccessToken()
Automatically call getApple(1) without disturbing the user
class Inceptor : Interceptor {
internal var token: String?=null
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request=chain.request()
request=request.newBuilder().build()
val response=chain.proceed(request)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// get a new token (I use a synchronous Retrofit call)
val requestBody=FormBody.Builder()
.add("UserName", “abcd")
.add("Password", “*****")
.build()
val newRequest=request.newBuilder()
.url("Put your url")
.post(requestBody)
.build()
val tokenRefreshResponse=chain.proceed(newRequest)
val newRetryHttpUrl=request.url()
if (tokenRefreshResponse.code() == HttpURLConnection.HTTP_OK) {
val retryOriginaResponseBody: RequestBody
val builder=FormBody.Builder()
retryOriginaResponseBody=builder.build()
val retryRequest: Request
if (request.method() == "POST") {
retryRequest=request.newBuilder()
.url(request.url())
.post(retryOriginaResponseBody)
.build()
} else {
retryRequest=request.newBuilder()
.url(newRetryHttpUrl)
.build()
}
val retryResponse=chain.proceed(retryRequest)
return retryResponse
} else {
return tokenRefreshResponse
}
}
}
return response
}
}

How to fetch data from network and not from http cache?

Here is my code , I have added CacheHeaderInterceptor but one of the requests
for some cases needs to do force call from network instead of retrieving cache response
but as I have added CacheHeaderInterceptor it never called after first call.
but I need to have check and based on that check fetch from network or retrieve cache response
#Singleton
#Provides
fun httpClient(context: Context, #Named(“UserPreferences”) preferences: SharedPreferences): OkHttpClient {
val appCacheDir = context.cacheDir
val httpCacheDir = File(appCacheDir, HTTP_CACHE_DIRNAME)
if (!httpCacheDir.exists()) {
httpCacheDir.mkdirs()
}
val builder = OkHttpClient.Builder()
val authInterceptor = LegacyAuthInterceptor(preferences, userAuthRelay)
builder.addNetworkInterceptor(authInterceptor)
if (BuildConfig.DEBUG) {
builder.addNetworkInterceptor(StethoInterceptor())
}
builder.addNetworkInterceptor(CacheHeaderInterceptor(isStoreUpdatedRelay))
return builder
.cache(Cache(httpCacheDir, MAX_HTTP_CACHE_SIZE))
.connectTimeout(30, SECONDS)
.writeTimeout(30, SECONDS)
.readTimeout(30, SECONDS)
.retryOnConnectionFailure(true)
.build()
}
class CacheHeaderInterceptor(private val isUpdatedRelay: BehaviorRelay<Boolean>) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
chain.request().headers().get(CustomCacheHeader.CUSTOM_CACHE_HEADER_KEY)
?: return chain.proceed(chain.request())
val maxAge = chain.request().headers().values(CustomCacheHeader.CUSTOM_CACHE_HEADER_KEY).firstOrNull()?.toLongOrNull()
val modifiedRequest = chain.request().newBuilder().removeHeader(CustomCacheHeader.CUSTOM_CACHE_HEADER_KEY).build()
val originalResponse = chain.proceed(modifiedRequest)
return when {
isUpdatedRelay.value -> {
val modifiedResponse = originalResponse.newBuilder()
.addHeader("Cache-Control", "no-cache")
.build()
isStoreUpdatedRelay.accept(false)
modifiedResponse
}
maxAge != null -> {
// Add Cache-Control to the response.
val modifiedResponse = originalResponse.newBuilder()
.removeHeader("Cache-Control")
.removeHeader("Pragma")
.addHeader("Cache-Control", "max-age=$maxAge")
.build()
modifiedResponse
}
else -> // Missing max-age, proceed with original response.
originalResponse
}
}
}
I found the solution for my question
adding addInterceptor()
There is addNetworkInterceptor() and addInterceptor().

Categories

Resources