Handling specific errors by sending another request transparently in retrofit - android

Here's the case I'm trying to handle,
If a request is executed, and the response indicates the auth token is expired,
send a refresh token request
if the refresh token request succeeds, retry the original request
This should be transparent to the calling Activity, Fragment... etc. From the caller's point of view, it's one request, and one response.
I've achieved this flow before when using OkHttpClient directly, but I don't know how to achieve this in Retrofit.
Maybe something related to this open issue about a ResponseInterceptor?
If there's no straight-forward way to achieve this in retrofit, what would be the best way to implement it? A base listener class?
I'm using RoboSpice with Retrofit as well, if it can be helpful in such case.

Since I'm using RoboSpice, I ended up doing this by creating an abstract BaseRequestListener.
public abstract class BaseRequestListener<T> implements RequestListener<T> {
#Override
public void onRequestFailure(SpiceException spiceException) {
if (spiceException.getCause() instanceof RetrofitError) {
RetrofitError error = (RetrofitError) spiceException.getCause();
if (!error.isNetworkError()
&& (error.getResponse().getStatus() == INVALID_ACCESS_TOKEN_STATUS_CODE)) {
//I'm using EventBus to broadcast this event,
//this eliminates need for a Context
EventBus.getDefault().post(new Events.OnTokenExpiredEvent());
//You may wish to forward this error to your listeners as well,
//but I don't need that, so I'm returning here.
return;
}
}
onRequestError(spiceException);
}
public abstract void onRequestError(SpiceException spiceException);
}

Related

Retrofit & RxJava2 refresh token and retry

I have a login endpoint by which I receive an authToken and a refreshToken. The first one expires in an hour, so I should use the second one to refresh it and continue the application flow.
Now my app is filled with retrofit calls all over the place and I potentially could get a 401 at any given moment, so how can I make it so every time I get a 401 a refresh token is issued, and then the original request retried?
This is my refresh signature:
#POST("/auth/actions/refresh")
fun refreshToken(#Body tokenRefresh: TokenRefresh): Single<LoginResponse>
I thought about making a base repository class with a method withAuth() that takes any Observable/Single/Flowable and then applies this logic but I cannot find a way to implement it.
Saw a bunch of implementations but none of them match my needs... can anyone push me in the right direction?
This is the closest I've found, however there seems to be some errors on the flatmapping
I just came across similar requirement and came up with following solution. It's pretty simple, it just makes one attempt to call REST endpoint and if that call fails with HTTP 401 it reauthenticates and repeats the call again. Otherwise it just emits the original error.
fun <T> Single<T>.withAuth() = retryWhen { errors ->
var firstAttempt = true
errors.flatMapSingle { error ->
if (firstAttempt && error is HttpException && error.code() == 401) {
firstAttempt = false
reauthenticate()
} else {
Single.error(it)
}
}
}
where the reauthentication function has the following signature:
fun reauthenticate(): Single<AnyAuthResponse>
Please note that the concrete exception type might depend on HTTP implementation you actually use, so you may want to update the condition to detect HTTP 401 response, but the code should give you an overall picture of how to solve your problem.
I think you can do this without modifying all calls. Add an Authenticator to your Retrofit
Refreshing OAuth token using Retrofit without modifying all calls
You can use Interceptor to intercept each request and check whether it returns 401 - UnAuthorised Access and iff then refresh the token and replay the current API request.
public final class SessionInterceptor implements Interceptor {
// gets intercept
#Override public Response intercept(#NonNull final Chain chain) throws IOException {
final Request request = chain.request();
final Response response = chain.proceed(request);
final ResponseBody responseBody = response.body();
if (response.code() == 401) {
synchronized (this) {
// Refresh your token
// Update your authToken + Refreshed token
final retrofit2.Response response = refreshToken();
}
}
// Replay the original request
// Perform request, here original request will be executed
final Request original = chain.request();
final Request.Builder builder = original.newBuilder();
// Set your new refreshed token
if (accessToken.isSet()) {
builder.header(AUTHORIZATION, String.format(BEARER,
accessToken.get()));
}
final Request request = builder.method(original.method(), original.body()).build();
return chain.proceed(request);
}
}

Ensuring a valid auth token is always available

I've been trying to figure out how to authenticate users for my android app. It is based on a website which already has a developed api, using JWT to authenticate.
I have come against the problem of refreshing tokens. Let's say I want to fetch something from the API and I need the auth token for that. I check my current auth token. If it is expired, I need to get a new one using some sort of refresh token.
However, it seems like almost no matter how I think of trying to implement it, I run into a few problems:
I don't want the UI thread to wait while I get a new token
I would prefer that I don't have to explicitly check whether the token
is there (and then refresh it) before making any API call
I've come up with one solution that solves #1 and at least minimizes the pain of #2. I can have some sort of getToken method. As an example, using JS style promises because they're easier for me to understand:
function getToken() {
return new Promise((resolve) => {
// Check for token, and return if valid.
// Otherwise, go to the server and get a new one
...
resolve(token)
}
}
// When making an API call
getToken().then((token) => {
// Call API
})
I think I can work this out so that the request will never be running on the UI thread, which solves #1, and as far as #2, it's at least bearable.
My question is this: is there a better way to do this? It kind of seems like AccountManager might be able to handle this sort of thing for me, but the documentation for it is subpar at best, so I'm not sure how I would even implement it. If AccountManager can do it and you know of a good tutorial for it, please comment with that.
A way to accomplish this is intercept a 401 status code and refresh token.
If you are using Volley, you can extend Request class and override parseNetworkEror(VolleyError error) method. If need be, schedule a Job which will refresh the token (JobDispatcher) and trigger an event to communicate UI about the change (EventBus).
The following example is using OAuth authentication, but can be easily changed to implement JWT.
#Override
protected VolleyError parseNetworkError(VolleyError volleyError) {
if (getDataAccess().shouldRefreshToken(volleyError)) {
if (!EventBus.getDefault().hasSubscriberForEvent(TokenRefreshedEvent.class)) {
EventBus.getDefault().register(this);
}
CSApplication app = CSApplication.getInstance();
FirebaseJobDispatcher dispatcher = app.getJobDispatcher(app.getApplicationContext());
Job myJob = dispatcher.newJobBuilder()
.setService(JobRefreshToken.class)
.setTag("REFRESH_TOKEN")
.setTrigger(Trigger.NOW)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setConstraints(Constraint.ON_ANY_NETWORK)
.build();
int result = dispatcher.schedule(myJob);
if (result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS) {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Scheduling job refresh token");
} else {
LogUtils.log(LogUtils.Type.JOB, GsonRequest.class, "Error on schedule refresh token");
}
}
return super.parseNetworkError(volleyError);
}
public boolean shouldRefreshToken(VolleyError error) {
boolean shouldRefreshToken = error.networkResponse != null && error.networkResponse.statusCode == 401;
if (shouldRefreshToken) {
Map<String, String> headers = error.networkResponse.headers;
if (headers.containsKey("WWW-Authenticate")) {
String value = headers.get("WWW-Authenticate");
boolean issuerInvalid = value.contains("The issuer is invalid");
shouldRefreshToken = !issuerInvalid;
if (issuerInvalid) {
log(LogUtils.Type.VOLLEY, DataAccess.class, "Issuer do token é inválido");
}
}
}
return shouldRefreshToken;
}
Job Code
getDataAccess().refreshToken(getApplicationContext(), new VolleyCallback<Void>() {
#Override
public void onSuccess(Void aVoid) {
EventBus.getDefault().post(new TokenRefreshedEvent(true));
job.jobFinished(params, false);
log(LogUtils.Type.JOB, JobRefreshToken.class, "Refresh Token job finished");
}
#Override
public void onError(VolleyError error) {
super.onError(error);
EventBus.getDefault().post(new TokenRefreshedEvent(false));
job.jobFinished(params, false);
}
});
return true;
}
What I ended up doing was creating a method getToken which either returns the current token or gets a new one (blocking). With this strategy, I need to make sure that it never gets called from the UI thread. I created a Retrofit2 interceptor which calls getToken. The benefit of this method is that I can just call my Retrofit methods without worrying about the token at all, and it checks for expiration and gets a new one as necessary.

How can I parse the response through Retrofit 2 if response can be both a JSon/HTML?

Hi I'm working with an API, which returns the login page when the session token is invalid/JSON response if the session is valid. How can I implement this with Retrofit, ie having multiple response types ?
P.S It's an old API and it can't be changed. I'm new to retrofit, I'll be really grateful of your help.
Would post this as comment as it's more a suggestion then actual answer but here it goes:
(If you're completely new to Retrofit leave a comment to explain it in more detail)
You could make your Call return a Response like so:
#GET("login/endpoint")
Call<Response> getLogin();
than you can make the call like this
Call<Response> getLogin = ApiService.getLogin();
getLogin.enqueue(new Callback<Void>() {
#Override
public void onResponse(Call<Response> call, Response<Response> response) {
//here you can access Response.body() and use it to determine wether it's json or html and react accordingly
}
#Override
public void onFailure(Call<Response> call, Throwable t) {
//todo: error message
}
});
note: the Response i've used is from the OkHttp Library.
If this doesn't work you could try to make your own converter which has a check for html/json and adding it in the creation of the retrofit instance. i'm not entirely sure how you can go about with that but this seems to have a general idea: custom converter from futurestudio.
If you need more guidance/clarification please let me know i'll be able to answer later today.

Merge and handle two RxJava Observable of different types

My goal
I want to check if the server's token is still valid, let's say I know that information just by calling this getter : preferenceHelper.isTokenValid(). Then, if the token is invalid, calling a request to get a new token and updating the token locally, THEN, proceed with the next request to post the point to the server. That's because I need a valid token in order to make any further server request.
Let say I have those two server request that returns Observable:
This request is meant to get the server token, then upon reception, updating it.
Observable<Response<EntityToken>> updateServerToken = retrofitApi.authenticate(username,password);
This request is meant to post the current location to the server, then if it succeed, return the saved point
Observable<Response<EntityPoint>> updateServerToken = retrofitApi.postPoint(point);
Issues i'm facing currently:
Both observable that needs to be merged are from different type
Executing the token update request only if it needs to
Waiting for the token update request to complete before executing the request to post points
How should I write my RxJava Observable to satisfy all those condition?
First, I would create a method that checks if the entityToken is valid or not. If valid, use Observable.just() but you have to create an instance of Response somehow. If invalid, then call the server using the API in your requirement retrofitApi.authenticate(). Either path is taken, the method getTokenObservable() emits Observable<Response<EntityToken>>.
public Observable<Response<EntityToken>> getTokenObservable(EntityToken entityToken, String username, String password) {
boolean isTokenValid = preferenceHelper.isTokenValid(entityToken);
if (isTokenValid) {
//my assumption that you have something like this
Response<EntityToken> responseToken = new Response<EntityToken>();
responseToken.setEntityToken(entityToken);
return Observable.just(new Response<EntityToken>(entityToken.class));
} else {
Observable<Response<EntityToken>> updateServerToken = retrofitApi.authenticate(username, password);
return updateServerToken;
}
}
and then when calling it, use flatMap() which take emisssions of Observable<Response<EntityToken>> and returns emissions of Observable<Response<EntityPoint>>. Subscribe and proceed as normal.
Observable<Response<EntityToken>> updatePointObservable = getTokenObservable(entityToken, username, password);
updatePointObservable
.flatMap(new Func1<Response<EntityToken>, Observable<Response<EntityPoint>>>() {
#Override
public Observable<Response<EntityPoint>> call(Response<EntityToken> responseToken) {
EntityToken entityToken = responseToken.getEntityToken(); //my assumption
saveTokenLocally(entityToken); //this is where you save your token locally, change to the right method that you have
Observable<Response<EntityPoint>> updateServerTokenObservable = retrofitApi.postPoint(point, entityToken); //pass your entityToken to the call?
return updateServerTokenObservable;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Response<EntityPoint>>() {
#Override
public void onCompleted() {
//your own logic
}
#Override
public void onError(Throwable e) {
//your own logic
}
#Override
public void onNext(Response<EntityPoint> entityPoint) {
//your own logic
}
});
As there is a dependency between the three calls, merge does not make any sense. instead, use flatMap:
Observable<Response<EntityPoint>> response =
retrofitApi.isTokenValid()
.flatMap(isValid ->
isValid
? Observable.just("")
: retrofitApi.authenticate(username,password)
.doOnNext(token -> doSomethingWithTheToken(token)
)
.flatMap(dummy -> retrofitApi.postPoint(point));

How to get the HTTP error code from a failed request in Retrofit 2.0.0?

I am making call using the following callback method:
Callback<PeopleList> callback = new Callback<PeopleList>() {
#Override
public void onResponse(Response<PeopleList> response) {
Toast.makeText(LoginActivity.this,getString(R.string.login_failed), Toast.LENGTH_SHORT).show();
}
#Override
public void onFailure(Throwable t) {
Toast.makeText(LoginActivity.this,getString(R.string.login_failed), Toast.LENGTH_SHORT).show();
}
};
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
retrofit.create(MyService.class).getPeopleData().enqueue(callback);
To the following interface:
public interface MyService {
#Headers("Accept: application/json")
#GET("/data/people/")
Call<PeopleList> getPeopleData();
}
This callback works just fine on successful requests. On unsuccessful ones however it does not give me the opportunity to investigate further as the onFailure method does not allow me to retrieve the http error code that came with the response.
On investigating further I found that according to several stackoverflow threads, the onResponse method should be called even on unsuccessful requests. This however seems to be at odds not only with my personal experience but also with the documentation of the Callback interface, which states that:
Communicates responses from a server or offline requests. One and only one method will be
invoked in response to a given request.
So the question is, how do I get the HTTP error code from a failed response if the onResponse method isn't called?
I think that the onResponse method gets called even if there is a response with an Error so something like this might work(sorry if I did something wrong first attempt to answer anybody :)
#Override
public void onResponse(Response<PeopleList> response) {
if(response.isSuccess()){ //good http request, do something with response.body()....
Toast.makeText(LoginActivity.this,getString(R.string.login_failed), Toast.LENGTH_SHORT).show();
} else { //bad http response do something with error message
try {
Toast.makeText(LoginActivity.this,response.errorBody().string().toString(), Toast.LENGTH_SHORT).show();
} catch (IOException e){
//IOException caught from response.errorBody().string() method
}
}
}
onResponse() will be always called, for failed requests .body() is null. response.isSuccess() is used to quickly distinguish requests with http codes between 200 and 300.
If you want to access http codes you can do the following:
int htppResultCode = response.raw().code();
It accesses the raw Response object which holds information about general outcome of the request.

Categories

Resources