If you’ve kept yourself up to date with the latest in Retrofit you’ve soon came to the question “Should I or shouldn’t I update to Retrofit 2?”. At Babbel we’ve already answered the question, we updated our new Android app to use Retrofit 2.
Although still in a beta stage, Retrofit 2 already offers a lot. However, there are some crucial changes. Perhaps one of the changes that affected us the most was the removal of RetrofitError
. There are more than one reason why this class was removed, but we weren’t ready to give up on it so fast. As an intermediate step we needed to port its behavior to Retrofit 2 while using RxJava.
The new Retrofit design helps you define call adapters that can help you customize how you handle errors in your calls.
The old way with Retrofit 1
If the API you’re working with is anything like most of the APIs out there, you’ll probably have some custom errors. Something like HTTP extended errors, where your call returns a non 2XX status code and the response body specifies the exact error. Picture a login call that returns the following error when the email is malformed:
HTTP Status: 400 BAD REQUEST
{
"error": {
"code": "600",
"title": "Bad request",
"detail": "The specified email is malformed."
}
}
The call returns HTTP 400 bad request and further specifies the reason inside the response body. Here the example follows a JSON API, but it’s easialy applied to any other format.
In most implementations, it is desirable to inspect the response body and react upon the error code. With RetrofitError
one could easily convert the error to a Java object with code similar to:
public void onError(Throwable throwable) {
if (throwable instanceof RetrofitError) {
RetrofitError error = (RetrofitError) throwable;
LoginErrorResponse response = ((LoginErrorResponse) error.getBodyAs(LoginErrorResponse.class));
// ...
}
}
Here throwable
is an exception thrown from the Retrofit call and LoginErrorResponse
is a Java class that can be serialized to the JSON shown above. The above code is part of an RxJava subscriber for the login call. One can than imagine it would be easy to access the field code
using the returned LoginErrorResponse
object.
Enter Retrofit 2
Before you proceed it’s important to notice that this article and the code in it was written using the following library versions:
// ...
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
// ...
The code might need some adaptation depending on the version you’re using. The Retrofit version is after all still in beta.
With Retrofit 2 one can use the following rules to somehow map the received exceptions into the former RetrofitError
.
- Exceptions with class
retrofit2.adapter.rxjava.HttpException
are of kindRetrofitError.Kind.HTTP
. This means your call returned a non 2XX status code. - Exceptions with class
java.io.IOException
are of kindRetrofitError.Kind.NETWORK
andRetrofitError.Kind.CONVERSION
. This means something went wrong with your call or (de)serialization. - All other exceptions are of kind
RetrofitError.Kind.UNKNOWN
There’s an obvious problem with the rules above – One doesn’t distinguish between conversion and network errors. In our use case this was not a problem and the solution shown here works for us. The idea is to convert these exceptions into a class that we can easily deserialize into a Java object.
The call adapter
RxJava offers a very nice API for error handling. It’s fairly straight forward to plug your subscribers into the stream and deal with the errors on the onError
method. From there it’s also easy to check the throwable’s class and implement the rules above.
Something like:
public void onError(Throwable throwable) {
if (throwable instanceof HttpException) {
// We had non-2XX http error
}
if (throwable instanceof IOException) {
// A network or conversion error happened
}
// We don't know what happened. We need to simply convert to an unknown error
// ...
}
Although this works we can further improve this using the new Retrofit feature – the CallAdapter
.
I’ve come accross this Gist where it’s shown how one can implement a class that would behave similarly to RetrofitError
. I take no credit for this implementation I simply want to paste here the relevant part:
public class RetrofitException extends RuntimeException {
public static RetrofitException httpError(String url, Response response, Retrofit retrofit) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, url, response, Kind.HTTP, null, retrofit);
}
public static RetrofitException networkError(IOException exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.NETWORK, exception, null);
}
public static RetrofitException unexpectedError(Throwable exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.UNEXPECTED, exception, null);
}
/** Identifies the event kind which triggered a {@link RetrofitException}. */
public enum Kind {
/** An {@link IOException} occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
private final String url;
private final Response response;
private final Kind kind;
private final Retrofit retrofit;
RetrofitException(String message, String url, Response response, Kind kind, Throwable exception, Retrofit retrofit) {
super(message, exception);
this.url = url;
this.response = response;
this.kind = kind;
this.retrofit = retrofit;
}
/** The request URL which produced the error. */
public String getUrl() {
return url;
}
/** Response object containing status code, headers, body, etc. */
public Response getResponse() {
return response;
}
/** The event kind which triggered this error. */
public Kind getKind() {
return kind;
}
/** The Retrofit this request was executed on */
public Retrofit getRetrofit() {
return retrofit;
}
/**
* HTTP response body converted to specified {@code type}. {@code null} if there is no
* response.
*
* @throws IOException if unable to convert the body to the specified {@code type}.
*/
public <T> T getErrorBodyAs(Class<T> type) throws IOException {
if (response == null || response.errorBody() == null) {
return null;
}
Converter<ResponseBody, T> converter = retrofit.responseConverter(type, new Annotation[0]);
return converter.convert(response.errorBody());
}
}
What’s good about this solution is that it uses the Retrofit
instance to get the correct response converter and convert the response body. What we need to do now is plug this class into a call adapter that converts the errors.
Our approach was to wrap the current RxJavaCallAdapterFactory
into a class that takes care of the error conversion. Basically we wanted a way to plug into the stream some logic that would convert the throwable into the above RetrofitException
class. The perfect Rx operator for this is the onErrorResumeNext
. This operator lets you plug into the stream a new observable when an error is received, but doesn’t stop the original emition of events. This means that whenever an error happens with a given call, the subscribers will still receive the error event. Here’s the code for this:
public class RxErrorHandlingCallAdapterFactory extends CallAdapter.Factory {
private final RxJavaCallAdapterFactory original;
private RxErrorHandlingCallAdapterFactory() {
original = RxJavaCallAdapterFactory.create();
}
public static CallAdapter.Factory create() {
return new RxErrorHandlingCallAdapterFactory();
}
@Override
public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
return new RxCallAdapterWrapper(retrofit, original.get(returnType, annotations, retrofit));
}
private static class RxCallAdapterWrapper implements CallAdapter<Observable<?>> {
private final Retrofit retrofit;
private final CallAdapter<?> wrapped;
public RxCallAdapterWrapper(Retrofit retrofit, CallAdapter<?> wrapped) {
this.retrofit = retrofit;
this.wrapped = wrapped;
}
@Override
public Type responseType() {
return wrapped.responseType();
}
@SuppressWarnings("unchecked")
@Override
public <R> Observable<?> adapt(Call<R> call) {
return ((Observable) wrapped.adapt(call)).onErrorResumeNext(new Func1<Throwable, Observable>() {
@Override
public Observable call(Throwable throwable) {
return Observable.error(asRetrofitException(throwable));
}
});
}
private RetrofitException asRetrofitException(Throwable throwable) {
// We had non-200 http error
if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
Response response = httpException.response();
return RetrofitException.httpError(response.raw().request().url().toString(), response, retrofit);
}
// A network error happened
if (throwable instanceof IOException) {
return RetrofitException.networkError((IOException) throwable);
}
// We don't know what happened. We need to simply convert to an unknown error
return RetrofitException.unexpectedError(throwable);
}
}
}
The above class wraps the CallAdapter
created by RxJavaCallAdapterFactory
and whenever there’s an error we convert the throwable to the class RetrofitException
. This happens inside the method adapt(Call<R> call)
. We first use the wrapped adapter to adapt to the call. This will return an Observable where we can plug the function that converts the throwable into a RetrofitException
.
To use this new call adapter we just configure our Retrofit instance like so:
new Retrofit.Builder()
.baseUrl("your base url")
.addConverterFactory(GsonConverterFactory.create(new Gson()))
.addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
.build();
From here on, all the subscribers that use this Retrofit instance will always receive on their onError
method instances of the class RetrofitException
. One can now cast the received throwable and use is similarly to the RetrofitError
:
public void onError(Throwable throwable) {
RetrofitException error = (RetrofitException) throwable;
LoginErrorResponse response = error.getBodyAs(LoginErrorResponse.class);
//...
}
error.getErrorBodyAs(LoginErrorResponse.class)
Summary
What we’ve shown here is a handy way of converting your custom API errors into Java objects while using Retrofit 2 with RxJava. The presented solution fits our use case and makes use of the CallAdapter
s from Retrofit 2 to remove the conversion code from the subscribers.
Photo by Michiel Leunens on Unsplash