Handling generic Http errors across your Android app
Using Otto and Retrofit to log out users when receiving 401 error responses from the API
I was recently working on an Android project where we needed to log out the current user whenever you encountered a 401 Unauthorized HTTP response from our API.
I am a fan of all the Android libraries from square, so for this post I’ll be using the REST client Retrofit in combination with the event bus Otto to achieve a centralized elegant way to do this.
Although all this should have been pretty straightforward there are a couple of gotchas to be aware of, mostly regarding Otto limitations so let’s get to it!
0. Create an event object for Otto
public class HttpUnauthorizedEvent { }
The way Otto works is by creating a contract between publishers and subscribers based on the type of the object used as a parameter. In this case we will use the HttpUnauthorizedEvent object to signal that a 401 error has happened in our application.
An instance of any class may be published on the bus and it will only be dispatched to subscribers for that type.
1. Create a custom ErrorHandler for Retrofit and post events for intercepted 401 errors using Otto event bus from there
public class CustomErrorHandler implements ErrorHandler { private static final Handler MAIN_LOOPER_HANDLER = new Handler(Looper.getMainLooper());
private final Bus eventBus; public CustomErrorHandler(Bus eventBus) {
this.eventBus = eventBus;
} @Override public Throwable handleError(RetrofitError error) {
Response r = error.getResponse();
if (r != null && r.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// we need to make sure we post in the UI thread
MAIN_LOOPER_HANDLER.post(new Runnable() {
@Override public void run() {
eventBus.post(new HttpUnauthorizedEvent());
}
});
}
return error;
}
}
One thing to notice is that we’re making sure we’re posting in the UI thread, this is mainly for two reasons:
By default, all interaction with an instance is confined to the main thread.
- If you try to post from a different thread Otto will throw an exception.
- We actually want to react to these errors in the UI thread, which is where we will be logging out the user (e.g. in an Activity).
2. Configure the CustomErrorHandler to be used by Retrofit RestAdapter
// Obtain Bus eventBus from constructor or singleton injection
Bus eventBus = new BusProvider().get();// Set retrofit ErrorHandler to use our CustomErrorHandler and
// pass the eventBus to it
RestAdapter restAdapter = new RestAdapter.Builder()
.setServer("API_URL")
.setErrorHandler(new CustomErrorHandler(eventBus))
.build();// create and use your retrofit REST client
RestApiClient api = restAdapter.create(RestApiClient.class);
api.MethodCall(...); //If a 401 error response happens here
// the error handler will automatically notify all subscribers
3. Subscribe for errors on a inner-class of your (base) Activity
For my specific case I wanted to put the subscription and log-out logic in a base Activity from which all my app Activities were inheriting. This is currently impossible given that Otto doesn’t support subscribing on base classes.
Otto will not traverse the class hierarchy and add methods from base classes or interfaces that are annotated. This is an explicit design decision to improve performance of the library as well as keep your code simple and unambiguous.
As a workaround you can put your @Subscribe method on your base Activity’s nested class and it will work as a charm.
public abstract class BaseActivity ... { private class AuthFailureHandler {
@Subscribe
public void onAuthFailure(HttpUnauthorizedEvent event) {
BaseActivity.this.logOutCurrentUser();
}
} // Obtain same Singleton eventBus
private Bus eventBus = new BusProvider().get();
private AuthFailureHandler authFailureHandler; @Override
protected void onResume() {
super.onResume();
authFailureHandler = new AuthFailureHandler();
// "In order to receive events, a class instance needs to
// register with the bus."
eventBus.register(authFailureHandler);
} @Override
protected void onPause() {
super.onPause();
// "Remember to also call the unregister method when
// appropriate."
eventBus.unregister(authFailureHandler);
} private void logOutCurrentUser() {
// use a (pending) Intent Service to log out the user
}
}
If there’s enough interest I can work out a simple code example and share it here.
Happy coding!