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:
publicvoidonError(Throwablethrowable){if(throwableinstanceofRetrofitError){RetrofitErrorerror=(RetrofitError)throwable;LoginErrorResponseresponse=((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:
publicvoidonError(Throwablethrowable){if(throwableinstanceofHttpException){// We had non-2XX http error}if(throwableinstanceofIOException){// 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:
publicclassRetrofitExceptionextendsRuntimeException{publicstaticRetrofitExceptionhttpError(Stringurl,Responseresponse,Retrofitretrofit){Stringmessage=response.code()+""+response.message();returnnewRetrofitException(message,url,response,Kind.HTTP,null,retrofit);}publicstaticRetrofitExceptionnetworkError(IOExceptionexception){returnnewRetrofitException(exception.getMessage(),null,null,Kind.NETWORK,exception,null);}publicstaticRetrofitExceptionunexpectedError(Throwableexception){returnnewRetrofitException(exception.getMessage(),null,null,Kind.UNEXPECTED,exception,null);}/** Identifies the event kind which triggered a {@link RetrofitException}. */publicenumKind{/** 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}privatefinalStringurl;privatefinalResponseresponse;privatefinalKindkind;privatefinalRetrofitretrofit;RetrofitException(Stringmessage,Stringurl,Responseresponse,Kindkind,Throwableexception,Retrofitretrofit){super(message,exception);this.url=url;this.response=response;this.kind=kind;this.retrofit=retrofit;}/** The request URL which produced the error. */publicStringgetUrl(){returnurl;}/** Response object containing status code, headers, body, etc. */publicResponsegetResponse(){returnresponse;}/** The event kind which triggered this error. */publicKindgetKind(){returnkind;}/** The Retrofit this request was executed on */publicRetrofitgetRetrofit(){returnretrofit;}/**
* 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>TgetErrorBodyAs(Class<T>type)throwsIOException{if(response==null||response.errorBody()==null){returnnull;}Converter<ResponseBody,T>converter=retrofit.responseConverter(type,newAnnotation[0]);returnconverter.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:
publicclassRxErrorHandlingCallAdapterFactoryextendsCallAdapter.Factory{privatefinalRxJavaCallAdapterFactoryoriginal;privateRxErrorHandlingCallAdapterFactory(){original=RxJavaCallAdapterFactory.create();}publicstaticCallAdapter.Factorycreate(){returnnewRxErrorHandlingCallAdapterFactory();}@OverridepublicCallAdapter<?>get(TypereturnType,Annotation[]annotations,Retrofitretrofit){returnnewRxCallAdapterWrapper(retrofit,original.get(returnType,annotations,retrofit));}privatestaticclassRxCallAdapterWrapperimplementsCallAdapter<Observable<?>>{privatefinalRetrofitretrofit;privatefinalCallAdapter<?>wrapped;publicRxCallAdapterWrapper(Retrofitretrofit,CallAdapter<?>wrapped){this.retrofit=retrofit;this.wrapped=wrapped;}@OverridepublicTyperesponseType(){returnwrapped.responseType();}@SuppressWarnings("unchecked")@Overridepublic<R>Observable<?>adapt(Call<R>call){return((Observable)wrapped.adapt(call)).onErrorResumeNext(newFunc1<Throwable,Observable>(){@OverridepublicObservablecall(Throwablethrowable){returnObservable.error(asRetrofitException(throwable));}});}privateRetrofitExceptionasRetrofitException(Throwablethrowable){// We had non-200 http errorif(throwableinstanceofHttpException){HttpExceptionhttpException=(HttpException)throwable;Responseresponse=httpException.response();returnRetrofitException.httpError(response.raw().request().url().toString(),response,retrofit);}// A network error happenedif(throwableinstanceofIOException){returnRetrofitException.networkError((IOException)throwable);}// We don't know what happened. We need to simply convert to an unknown errorreturnRetrofitException.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:
newRetrofit.Builder().baseUrl("your base url").addConverterFactory(GsonConverterFactory.create(newGson())).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
:
publicvoidonError(Throwablethrowable){RetrofitExceptionerror=(RetrofitException)throwable;LoginErrorResponseresponse=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.