I have an app which requires session (cookies) to process web calls. Im using Retrofit+RxJava. However, session could expire (Retrofit error with 401 Unauthorized status) and i want to reauthenticate (to get fresh cookies) and retry previous call in this case. How would i do it with RxJava?
My example:
getServerApi().getDialogs(offset, getCookies())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.retryWhen(observable -> {...}) // Need some logic
.subscribe(dialogsEnvelope -> getView().setDialogs(dialogsEnvelope),
throwable -> getView().setError(processFail(throwable)));
While an Interceptor may be a better solution for this particular problem, the question specifically asked for a solution using retryWhen, so here is is one way to do it:
retryWhen(new Func1<Observable<Throwable>, Observable<?>>(){
#Override
public void Observable<?> call(Observable<Throwable>> attempts) {
return attempts.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(Throwable throwable) {
if (throwable instanceof RetrofitError) {
RetrofitError retrofitError = (RetrofitError) throwable;
if (retrofitError.getKind() == RetrofitError.Kind.HTTP && retrofitError.getResponse().getStatus() == 401) {
// this is the error we care about - to trigger a retry we need to emit anything other than onError or onCompleted
return Observable.just(new Object());
} else {
// some other kind of error: just pass it along and don't retry
return Observable.error(throwable);
}
} else {
// some other kind of error: just pass it along and don't retry
return Observable.error(throwable);
}
}
});
}
})
However, your getCookies would not be called again in the case of a simple retry. That would just resubscribe to the same Observable but getCookies was called before the creation of that Observable. So I think you would have to wrap the creation of the source Observable in a defer.
Use OkHttp's extremely powerful Interceptor.
public class RecoverInterceptor implements Interceptor {
String getAuth() {
// check if we have auth, if not, authorize
return "Bearer ...";
}
void clearAuth() {
// clear everything
}
#Override public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
if (request.urlString().startsWith("MY ENDPOINT")) {
final Request signed = request.newBuilder()
.header("Authorization", getAuth())
.build();
final Response response = chain.proceed(signed);
if (response.code() == 401) {
clearAuth();
return intercept(chain);
} else {
return response;
}
} else {
return chain.proceed(request);
}
}
}
Remember to synchronize your auth process code, so that two concurrent requests do not invoke it at the same time.
While browsing the Internet for discovering a proper answer - i've found this cool gist describing how to refresh OAuth token with the help of OkHttp Interceptor (similar to accepted answer, but more complete).
It has nothing to do with RxJava, but for me it's more acceptable because i don't have to wrap each Observable with retryWith logic - everything is done on lower level (OkHttp library).
Related
I want to design API calls in such a way that it will be easy to handle success and failure responses easily from one place (instead of writing same code of call function for all APIs)
Here are the scenarios which I want to consider.
Handle success / failure and error responses like 4xx, 5xx etc of all APIs at one central place.
Want to cancel enqueue requests and also stop processing response if request is already sent in case of logout (because response parsing will modify some global data of app)
If access token has expired and 401 response received from cloud, it should get new token and then call API again automatically with new token.
My current implementation is not satisfying above requirements.
Is there any way to implement API calls which satisfy above requirements using Retrofit ?
Please suggest me a good design for this.
Here is my current implementation :
ApiInterface.java - It is an interface which contains different API calls definitions.
ApiClient.java - To get retrofit client object to call APIs.
ApiManager.java - It has methods to call APIs and parse their responses.
ApiInterface.java
public interface ApiInterface {
// Get Devices
#GET("https://example-base-url.com" + "/devices")
Call<ResponseBody> getDevices(#Header("Authorization) String token);
// Other APIs......
}
ApiClient.java
public class ApiClient {
private static Retrofit retrofitClient = null;
static Retrofit getClient(Context context) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), systemDefaultTrustManager())
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
retrofitClient = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build();
}
}
ApiManager.java
public class ApiManager {
private static ApiManager apiManager;
public static ApiManager getInstance(Context context) {
if (apiManager == null) {
apiManager = new ApiManager(context);
}
return apiManager;
}
private ApiManager(Context context) {
this.context = context;
apiInterface = ApiClient.getClient(context).create(ApiInterface.class);
}
public void getDevices(ResponseListener listener) {
// API call and response handling
}
// Other API implementation
}
Update :
For 1st point, interceptor will be helpful to handle 4xx, 5xx responses globally according to this.
But interceptor will be in the ApiClient file and to inform UI or API caller component, need to pass success or failure result in callback I mean response listener.
How can I do that ? Any idea ?
For 3rd point, I know little bit about Retrofit Authenticator. I think for this point it is suitable but it requires synchronous call to get new token using refresh token.
How can I make asynchronous call to synchronous ? (Note : this call is not retrofit call)
By handling the success/failure responses at a central place I'll assume you want to get rid of repeated boilerplate based on the error parsing logic and how it may create UI side-effects for your app.
I'd probably suggest keeping things really simple by creating a custom abstraction for Callback which invokes your APIs for success/failure according to your domain logic.
Here's something fairly simple implementation for use-case (1) :
abstract class CustomCallback<T> implements Callback<T> {
abstract void onSuccess(T response);
abstract void onFailure(Throwable throwable);
#Override
public void onResponse(Call<T> call, Response<T> response) {
if (response.isSuccessful()) {
onSuccess(response.body());
} else {
onFailure(new HttpException(response));
}
}
#Override
public void onFailure(Call<T> call, Throwable t) {
onFailure(t);
}
}
For use-case (2), to be able to cancel all enqueued calls upon a global event like logout you'd have to keep a reference to all such objects. Fortunately, Retrofit supports plugging in a custom call factory okhttp3.Call.Factory
You could use your implementation as a singleton to hold a collection of calls and in the event of a logout notify it to cancel all requests in-flight. Word of caution, do use weak references of such calls in the collection to avoid leaks/references to dead calls. (also you might want to brainstorm on the right collection to use or a periodic cleanup of weak references based on the transactions)
For use-case (3), Authenticator should work out fine since you've already figured out the usage there are 2 options -
Migrate the refresh token call to OkHttp/Retrofit and fire it synchronously
Use a count-down latch to make the authenticator wait for the async call to finish (with a timeout set to connection/read/write timeout for the refresh token API call)
Here's a sample implementation:
abstract class NetworkAuthenticator implements Authenticator {
private final SessionRepository sessionRepository;
public NetworkAuthenticator(SessionRepository repository) {
this.sessionRepository = repository;
}
public Request authenticate(#Nullable Route route, #NonNull Response response) {
String latestToken = getLatestToken(response);
// Refresh token failed, trigger a logout for the user
if (latestToken == null) {
logout();
return null;
}
return response
.request()
.newBuilder()
.header("AUTHORIZATION", latestToken)
.build();
}
private synchronized String getLatestToken(Response response) {
String currentToken = sessionRepository.getAccessToken();
// For a signed out user latest token would be empty
if (currentToken.isEmpty()) return null;
// If other calls received a 401 and landed here, pass them through with updated token
if (!getAuthToken(response.request()).equals(currentToken)) {
return currentToken;
} else {
return refreshToken();
}
}
private String getAuthToken(Request request) {
return request.header("AUTHORIZATION");
}
#Nullable
private String refreshToken() {
String result = null;
CountDownLatch countDownLatch = new CountDownLatch(1);
// Make async call to fetch token and update result in the callback
// Wait up to 10 seconds for the refresh token to succeed
try {
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
abstract void logout();
}
I hope this helps with your network layer implementation
So, With the help of official sample in the retrofit github repository here: https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java
The ErrorHandlingAdapter is the closest you can get to your requirement because it lets you control enqueuing of the call, creating the error callbacks, calling error callbacks on your own. Whether you want the caller to do some action or you want to handle it yourself in one place or just both.
So this is how you can create it. Do read the inline comments to understand.
public final class ErrorHandlingAdapter {
/**
* Here you'll decide how many methods you want the caller to have.
*/
interface MyCallback<T> {
void success(Response<T> response);
void error(String s);
}
/**
* This is your call type
*/
interface MyCall<T> {
void cancel();
void enqueue(MyCallback<T> callback);
#NotNull
MyCall<T> clone();
}
public static class ErrorHandlingCallAdapterFactory extends CallAdapter.Factory {
#Override
public #Nullable
CallAdapter<?, ?> get(
#NotNull Type returnType, #NotNull Annotation[] annotations, #NotNull Retrofit retrofit) {
if (getRawType(returnType) != MyCall.class) {
return null;
}
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalStateException(
"MyCall must have generic type (e.g., MyCall<ResponseBody>)");
}
Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
Executor callbackExecutor = retrofit.callbackExecutor();
return new ErrorHandlingCallAdapter<>(responseType, callbackExecutor);
}
private static final class ErrorHandlingCallAdapter<R> implements CallAdapter<R, MyCall<R>> {
private final Type responseType;
private final Executor callbackExecutor;
ErrorHandlingCallAdapter(Type responseType, Executor callbackExecutor) {
this.responseType = responseType;
this.callbackExecutor = callbackExecutor;
}
#NotNull
#Override
public Type responseType() {
return responseType;
}
#Override
public MyCall<R> adapt(#NotNull Call<R> call) {
return new MyCallAdapter<>(call, callbackExecutor);
}
}
}
static class MyCallAdapter<T> implements MyCall<T> {
private final Call<T> call;
private final Executor callbackExecutor;
MyCallAdapter(Call<T> call, Executor callbackExecutor) {
this.call = call;
this.callbackExecutor = callbackExecutor;
}
#Override
public void cancel() {
call.cancel();
}
#Override
public void enqueue(final MyCallback<T> callback) {
if (!SomeCondition.myCondition) {
// Don't enqueue the call if my condition doesn't satisfy
// it could be a flag in preferences like user isn't logged in or
// some static flag where you don't want to allow calls
return;
}
call.clone().enqueue(
new Callback<T>() {
#Override
public void onResponse(#NotNull Call<T> call, #NotNull Response<T> response) {
callbackExecutor.execute(() -> {
int code = response.code();
if (code >= 200 && code < 300) {
//success response
callback.success(response);
} else if (code == 401) {
// Unauthenticated so fetch the token again
getTheTokenAgain(callback);
} else if (code >= 400 && code < 500) {
//handle error the way you want
callback.error("Client error");
} else if (code >= 500 && code < 600) {
//handle error the way you want
callback.error("Server error");
} else {
//handle error the way you want
callback.error("Something went wrong");
}
});
}
#Override
public void onFailure(#NotNull Call<T> call, #NotNull Throwable t) {
callbackExecutor.execute(() -> {
if (t instanceof IOException) {
callback.error("IOException");
} else {
callback.error("Some exception");
}
});
}
});
}
private void getTheTokenAgain(MyCallback<T> callback) {
// Make the call to get the token & when token arrives enqueue it again
// Don't forget to put termination condition like 3 times, if still not successful
// then just log user out or show error
// This is just dummy callback, you'll need to make a
// call to fetch token
new MyTokenCallback() {
#Override
public void onTokenArrived(String token) {
//enqueue(callback); here
}
#Override
public void onTokenFetchFailed() {
callbackExecutor.execute(() -> {
callback.error("Counld't fetch token");
});
}
};
// This is for demo you should put it in success callback
SomeCondition.callCount++;
Log.d("MG-getTheTokenAgain", "Method called");
if (SomeCondition.callCount < 3) {
enqueue(callback);
} else {
callbackExecutor.execute(() -> {
callback.error("Counld't fetch token");
});
}
}
#NotNull
#Override
public MyCall<T> clone() {
return new MyCallAdapter<>(call.clone(), callbackExecutor);
}
}
}
This is how you'll plug this adapter:
private void makeApiCall() {
//This is just for demo to generate 401 error you won't need this
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain -> {
Request request = chain.request().newBuilder()
.addHeader("Accept","application/json")
.addHeader("Authorization", "cdsc").build();
return chain.proceed(request);
});
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl("http://httpbin.org/")
.addCallAdapterFactory(new ErrorHandlingAdapter.ErrorHandlingCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build();
HttpBinService service = retrofit.create(HttpBinService.class);
ErrorHandlingAdapter.MyCall<Ip> ip = service.getIp();
ip.enqueue(
new ErrorHandlingAdapter.MyCallback<Ip>() {
#Override
public void success(Response<Ip> response) {
Log.d("MG-success", response.toString());
}
#Override
public void error(String s) {
Log.d("MG-error", s);
}
});
}
You might have to bend some things to your needs, but I think this could be good reference because it's in the official sample.
1. Handle success / failure and error responses like 4xx, 5xx etc of
all APIs at one central place.
Create following two classes:
ApiResponse.kt
class ApiResponse<T : Any> {
var status: Boolean = true
var message: String = ""
var data: T? = null
}
ApiCallback.kt
abstract class ApiCallback<T : Any> : Callback<ApiResponse<T>> {
abstract fun onSuccess(response: ApiResponse<T>)
abstract fun onFailure(response: ApiResponse<T>)
override fun onResponse(call: Call<ApiResponse<T>>, response: Response<ApiResponse<T>>) {
if (response.isSuccessful && response.body() != null && response.code() == 200) {
onSuccess(response.body()!!)
} else { // handle 4xx & 5xx error codes here
val resp = ApiResponse<T>()
resp.status = false
resp.message = response.message()
onFailure(resp)
}
}
override fun onFailure(call: Call<ApiResponse<T>>, t: Throwable) {
val response = ApiResponse<T>()
response.status = false
response.message = t.message.toString()
onFailure(response)
}
}
Now use the above ApiCallback class instead of Retrofit's Callback class to enqueue
2. Want to cancel enqueue requests and also stop processing response if request is already sent in case of logout (because response parsing will modify some global data of app)
You cannot stop processing a response midway, but what you can do is not update the ui or activity if the activity in question is not in foreground, which can be done with the help of LiveData from the MVVM Architecture.
3. If access token has expired and 401 response received from cloud, it should get new token and then call API again automatically with new token.
Create a TokenAuthenticator.java class like this
public class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
newAccessToken = service.refreshToken();
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header(AUTHORIZATION, newAccessToken)
.build();
}
#Override
public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
// Null indicates no attempt to authenticate.
return null;
}
}
Attach an instance of the above authenticator to OkHttpClient like this
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);
Then finally, attach okHttpClient to the Retrofit instance as you have already done
More info regarding the authenticator part can be found in this answer here
I'm using a custom Interceptor along with Retrofit client in my Android app, that throws an Exception under some specific circumstances. I'm trying to make it work using Kotlin coroutines.
The problem is that I'm unable to handle the before mentioned error, since in the moment the exception is thrown from within the Interceptor instance, it crashes the whole app instead of being caught in the coroutine's try/catch statement. While I was using the Rx implementation, the exception was flawlessly propagated to the onError callback where I was able to handle it the way I needed.
I guess this is somehow related to the underlying threads that are being used for the network call, please see the logs below from the place where the call is made, from the interceptor just before throwing the exception, and the stacktrace:
2019-11-04 17:17:34.515 29549-29729/com.app W/TAG: Running thread: DefaultDispatcher-worker-1
2019-11-04 17:17:45.911 29549-29834/com.app W/TAG: Interceptor thread: OkHttp https://some.endpoint.com/...
2019-11-04 17:17:45.917 29549-29834/com.app E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
Process: com.app, PID: 29549
com.app.IllegalStateException: Passed refresh token can\'t be used for refreshing the token.
at com.app.net.AuthInterceptor.intercept(AuthInterceptor.kt:33)
What am I supposed to do in order to be able to catch and handle this exception from the Interceptor correctly? Am I missing something?
You should subclass IOException and use that to send information from your interceptors to your calling code.
We consider other exceptions like IllegalStateException to be application crashes and do not send them over thread boundaries because we don’t want to burden most callers with catching them.
You may catch the exception in your custom Interceptor and return an empty response with some specific message and code. I have implemented a custom Interceptor to handle the situation like when you do not have or slow internet connection etc... Actually coroutine's suspend functions throws exception when dealing with network calls. In my experience, you can follow 2 approaches. 1. wrap your all network call in try...catch or 2. create a custom Interceptor and handle exceptions there and return some specific response.
Approach 1:
try {
webservice.login(username, password)
} catch (e: Exception) {
//...
}
Approach 2:
Create a custom Interceptor and handle exception there.
class LoggingInterceptor : Interceptor {
#Throws(Exception::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
try {
val response = chain.proceed(request)
val bodyString = response.body()!!.string()
return response.newBuilder()
.body(ResponseBody.create(response.body()?.contentType(), bodyString))
.build()
} catch (e: Exception) {
e.printStackTrace()
var msg = ""
when (e) {
is SocketTimeoutException -> {
msg = "Timeout - Please check your internet connection"
}
is UnknownHostException -> {
msg = "Unable to make a connection. Please check your internet"
}
is ConnectionShutdownException -> {
msg = "Connection shutdown. Please check your internet"
}
is IOException -> {
msg = "Server is unreachable, please try again later."
}
is IllegalStateException -> {
msg = "${e.message}"
}
else -> {
msg = "${e.message}"
}
}
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(999)
.message(msg)
.body(ResponseBody.create(null, "{${e}}")).build()
}
}
}
I have created gist for complete implementation of LoggingInterceptor with print logs of request and response. LoggingInterceptor
I dont know what exactly you need, but understood like this:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
okhttp3.Response response = chain.proceed(request);
// todo deal with the issues the way you need to
if (response.code() == SomeCode) {
//do something
return response;
}
return response;
}
})
.build();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(url)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder.build();
I'm using Retrofit + Rxjava to get a list of entities from server, by my design when the task fails, first it checks Internet connection and after that it checks the connection to the server in doOnError method of Observable.
When Client is not connected to the Internet doOnError is invoked in a reasonable time and user get the error message but the problem is when Internet is connected and I get wrong port or domain (to check the server problem error) It takes a long time (about 1 min or longer) and it's really annoying.
How can I reduce this time and what's the reason?
how I check Internet and Server connection :
public static boolean checkConnection(String ipOrUrl, int port) {
try {
int timeoutMs = 100;
Socket socket = new Socket();
SocketAddress soketAddress = new InetSocketAddress(ipOrUrl, port);
socket.connect(soketAddress, timeoutMs);
socket.close();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
how I try to get list of entities :
foodgetRetorfitService.getRestaurants()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
//here I'm checking the internet and server connection in the onlineTask if retrofit fail
//and if clients get Online this ( getRestaurantFromServer ) is called again.
.doOnError(error -> {
error.printStackTrace();
NetworkUtils.doOnlineTask(new OnlineTask() {
public void doWhenOnline() {
getResturantsFromServer();
}
}, true);
})
.subscribe(new Observer<List<Restaurant>>() {
#Override
public void onNext(List<Restaurant> restaurants) {
restaurantItemAdapter.updateAdapterData(restaurants);
}
#Override
public void onError(Throwable e) {
Log.e(TAG, "onError: rxjava and retrofit error : can't get restaurant list");
e.printStackTrace();
}
});
how doOnlineTask is Implemented
public static void doOnlineTask(OnlineTask onlineTask, boolean autoRetry, int retryTimeout) {
NetworkUtils.isOnline(autoRetry)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(error -> {
//here I check the Exception type ( I raised them when checking the connection )
if (error instanceof InternetConnectionException)
onlineTask.doWhenInternetFaild();
if (error instanceof ServerConnectionException)
onlineTask.doWhenServerFaild();
})
.retryWhen(t -> t.delay(2, TimeUnit.SECONDS))
.subscribe(result -> {
if (result.equals(NetworkStatus.CONNECTED))
onlineTask.doWhenOnline();
else {
if (result.equals(NetworkStatus.INTERNET_FAILD))
onlineTask.doWhenInternetFaild();
else if (result.equals(NetworkStatus.SERVER_FAILD))
onlineTask.doWhenInternetFaild();
}
}, error -> error.printStackTrace()
);
}
onlineTask is just an abstract class
abstract public void doWhenOnline();
public void doWhenInternetFaild() {
//implemented somehow
}
public void doWhenServerFaild() {
//implemented somehow
}
What I tried :
I guessed it's timeout problem so I changed Retrofit timeout with OkHttpClient and It does not worked. also I changed timeouts set by myself and I reduced them. not working.
I assume you are using OkHttp client to go with Retrofit. What you are looking for is most probably a connection timeout.
You can set different timeouts when building a client.
OkHttpClient client = new OkHttpClient().newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();
I'm using RxJava, Retrofit and MockWebServer I'm doing unit tests on my services.
My restAdapter have a custom error handler returning a custom throwable depending on the error code :
RestAdapter restAdapter = new RestAdapter.Builder().setErrorHandler(new RetrofitErrorHandler(context))
I tried to use a TestSubscriber and to subscribe like this:
TestSubscriber<X> testSubscriber = new TestSubscriber<>();
Observable<X> observable = mService.myCall(null, email);
observable.subscribe(testSubscriber);
but the .getOnErrorEvents() returns 0 event.
What should I do ?
Retrofit returns as a successful onNext event any kind of response, even if the status code differs from 2XX.
I propose to map the response and manually throw an error if the response.isSuccess() method returns false.
public Observable<UserEntity> loginUser(UserEntity user, String password) {
return this.restApi.doLogin(user.getEmail(), password)
.map(new Func1<Response<UserEntity>, UserEntity>() {
#Override
public UserEntity call(Response<UserEntity> userEntityResponse) {
if (!userEntityResponse.isSuccess()) throw new RuntimeException();
return userEntityResponse.body();
}
});
}
I am using OkHttp in my android application with several async requests. All requests require a token to be sent with the header. Sometimes I need to refresh the token using a RefreshToken, so I decided to use OkHttp's Authenticator class.
What will happen when 2 or more async requests get a 401 response code from the server at the same time? Would the Authenticator's authenticate() method be called for each request, or it will only called once for the first request that got a 401?
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
How to refresh token only once?
Use a singleton Authenticator
Make sure the method you use to manipulate the token is Synchronized
Count the number of retries to prevent excessive numbers of refresh
token calls
Make sure the API calls to get a fresh token and the
local storage transactions to save the new token in your local stores are not asynchronous. Or if you want to make them asynchronous make sure you to you token related stuff after they are completed.
Check if the access token is refreshed by another thread already to
avoid requesting a new access token from back-end
Here is a sample in Kotlin
#SingleTon
class TokenAuthenticator #Inject constructor(
private val tokenRepository: TokenRepository
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
return if (isRequestRequiresAuth(response)) {
val request = response.request()
authenticateRequestUsingFreshAccessToken(request, retryCount(request) + 1)
} else {
null
}
}
private fun retryCount(request: Request): Int =
request.header("RetryCount")?.toInt() ?: 0
#Synchronized
private fun authenticateRequestUsingFreshAccessToken(
request: Request,
retryCount: Int
): Request? {
if (retryCount > 2) return null
tokenRepository.getAccessToken()?.let { lastSavedAccessToken ->
val accessTokenOfRequest = request.header("Authorization") // Some string manipulation needed here to get the token if you have a Bearer token
if (accessTokenOfRequest != lastSavedAccessToken) {
return getNewRequest(request, retryCount, lastSavedAccessToken)
}
}
tokenRepository.getFreshAccessToken()?.let { freshAccessToken ->
return getNewRequest(request, retryCount, freshAccessToken)
}
return null
}
private fun getNewRequest(request: Request, retryCount: Int, accessToken: String): Request {
return request.newBuilder()
.header("Authorization", "Bearer " + accessToken)
.header("RetryCount", "$retryCount")
.build()
}
private fun isRequestRequiresAuth(response: Response): Boolean {
val header = response.request().header("Authorization")
return header != null && header.startsWith("Bearer ")
}
}
I see here two scenarios based on how API which you call works.
First one is definitely easier to handle - calling new credentials (e.g. access token) doesn't expire old one. To achieve it you can add an extra flag to your credentials to say that credentials are being refreshed. When you got 401 response, you set flag to true, make a request to get new credentials and you save them only if flag equals true so only first response will be handled and rest of them will be ignored. Make sure that your access to flag is synchronized.
Another scenario is a little bit more tricky - every time when you call new credentials old one are set to be expired by server side. To handle it you I would introduce new object to be used as a semafore - it would be blocked every time when 'credentials are being refreshed'. To make sure that you'll make only one 'refresh credentials' call, you need to call it in block of code which is synchronized with flag. It can look like it:
synchronized(stateObject) {
if(!stateObject.isBeingRefreshed) return;
Response response = client.execute(request);
apiClient.setCredentials(response.getNewCredentials());
stateObject.isBeingRefreshed = false;
}
As you've noticed there is an extra check if(!stateObject.isBeingRefreshed) return; to cancel requesting new credentials by following requests which received 401 response.
In my case I implemented the Authenticator using the Singleton pattern. You can made synchronized that method authenticate. In his implementation, I check if the token from the request (getting the Request object from Response object received in the params of authenticate method) is the same that the saved in the device (I save the token in a SharedPreferences object).
If the token is the same, that means that it has not been refresed yet, so I execute the token refresh and the current request again.
If the token is not the same, that means that it has been refreshed before, so I execute the request again but using the token saved in the device.
If you need more help, please tell me and I will put some code here.
This is my solution to make sure to refresh token only once in a multi-threading case, using okhttp3.Authenticator:
class Reauthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response?): Request? {
if (response == null) return null
val originalRequest = response.request()
if (originalRequest.header("Authorization") != null) return null // Already failed to authenticate
if (!isTokenValid()) { // Check if token is saved locally
synchronized(this) {
if (!isTokenValid()) { // Double check if another thread already saved a token locally
val jwt = retrieveToken() // HTTP call to get token
saveToken(jwt)
}
}
}
return originalRequest.newBuilder()
.header("Authorization", getToken())
.build()
}
}
You can even write a unit test for this case, too! 🎉
Add synchronized to authenticate() method signature.
And make sure getToken() method is blocking.
#Nullable
#Override
public synchronized Request authenticate(Route route, Response response) {
String newAccessToken = getToken();
return response.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken)
.build();
}
Make sure to use singleton custom Authenticator
When refreshing token successful return request with new token else return null.
class TokenAuthenticator(
private val sharedPref: SharedPref,
private val tokenRefreshApi: TokenRefreshApi
) : Authenticator,
SafeApiCall {
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
when (val tokenResponse = getUpdatedToken()) {
is Resource.Success -> {
val token = tokenResponse.data.token
sharedPref.saveToken(token)
response.request.newBuilder().header("Authorization", "Bearer $token").build()
}
else -> {
null
}
}
}
}
private suspend fun getUpdatedToken(): Resource<LoginResponse> {
return safeApiCall { tokenRefreshApi.refreshToken("Bearer ${sharedPref.getToken()}") }
}
}