android – Retrofit and Centralized Error Handling-ThrowExceptions

Exception or error:

Each request to the server may return error_code. I want to handle these error in one place
when I was using AsyncTask I had a BaseAsyncTask like that

public abstract class BaseAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {

    protected Context context;
    private ProgressDialog progressDialog;
    private Result result;

    protected BaseAsyncTask(Context context, ProgressDialog progressDialog) {
        this.context = context;
        this.progressDialog = progressDialog;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override
    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        HttpResponse<ErrorResponse> response = (HttpResponse<ErrorResponse>) result;
     if(response.getData().getErrorCode() != -1) {
                handleErrors(response.getData());
        }else 
            onResult(result);

    }

    private void handleErrors(ErrorResponse errorResponse) {
    }
     public abstract void onResult(Result result);
    }

But, using retrofit each request has its error handling callback:

                    git.getFeed(user,new Callback<gitmodel>() {
                    @Override
                    public void success(gitmodel gitmodel, Response response) {

                    }

                    @Override
                    public void failure(RetrofitError error) {

                    }
                });
            }
        });

How can I handle all errors in one place?

How to solve:

If you need to get some ‘logic’ error, then you need some Java logic since it’s not a Retrofit feature so basically:

  1. Create a Your implementation Callback that implements the Retrofit Callback
  2. Create a base object that define the method ‘isError’
  3. Modify Retrofit RestAdapter in order to get your Callback instead of the Retrofit One

MyCallback.java

import android.util.Log;
import retrofit.Callback;
import retrofit.client.Response;

public abstract class MyCallback<T extends MyObject> implements Callback<T> {

    @Override
    public final void success(T o, Response response) {
        if (o.isError()) {
            // [..do something with error]
            handleLogicError(o);
        }
        else {
            handleSuccess(o, response);
        }
    }

    abstract void handleSuccess(T o, Response response);

    void handleLogicError(T o) {
        Log.v("TAG", "Error because userId is " + o.id);
    }
}

MyObject.java (the base class for all your objects you get from Retrofit)

public class MyObject {
    public long id;
    public boolean isError() {
        return id == 1;
    }
}

MyRealObject.java – a class that extends the base object

public class MyRealObject extends MyObject {
    public long userId;
    public String title;
    public String body;
}

RetroInterface.java – the interface used by retrofit you should be familiar with

import retrofit.http.GET;
import retrofit.http.Path;

public interface RetroInterface {

    @GET("/posts/{id}")
    void sendGet(@Path("id") int id, MyCallback<MyRealObject> callback);

}

And finally the piece of code where you use all the logic

RestAdapter adapter = new RestAdapter.Builder()
    .setEndpoint("http://jsonplaceholder.typicode.com")
    .build();

RetroInterface itf = adapter.create(RetroInterface.class);
itf.sendGet(2, new MyCallback<MyRealObject>() {
    @Override
    void handleSuccess(MyRealObject o, Response response) {
        Log.v("TAG", "success");
    }

    @Override
    public void failure(RetrofitError error) {
        Log.v("TAG", "failure");
    }
});

If you copy and paste this code, you’ll get an error when you’ll execute the itf.sendGet(1, new MyCallback..) and a success for itf.sendGet(2, new MyCallback...)

###

Not sure I understood it correctly, but you could create one Callback and pass it as a parameter to all of your requests.

Instead of:

git.getFeed(user,new Callback<gitmodel>() {
    @Override 
    public void success(gitmodel gitmodel, Response response) { 

    } 

    @Override 
    public void failure(RetrofitError error) { 

    }
}); 

First define your Callback:

Callback<gitmodel> mCallback = new Callback<gitmodel>() {
    @Override 
    public void success(gitmodel gitmodel, Response response) { 

    } 

    @Override 
    public void failure(RetrofitError error) { 
        // logic to handle error for all requests
    } 
};

Then:

git.getFeed(user, mCallback);

###

In Retrofit you can specify ErrorHandler to all requests.

public class ApiErrorHandler implements ErrorHandler {

    @Override
    public Throwable handleError(RetrofitError cause) {
        //here place your logic for all errors
        return cause;
    }
}

Apply it to RestAdapter

RestAdapter.Builder()
            .setClient(client)
            .setEndpoint(endpoint)
            .setErrorHandler(errorHandler)
            .build();

I think that it is what you asked for.

###

In Retrofit2 you can’t set an ErrorHandler with the method .setErrorHandler(), but you can create an interceptor to fork all possible errors centralised in one place of your application.

With this example you have one centralised place for your error handling with Retrofit2 and OkHttpClient. Just reuse the Retrofit object (retrofit).

You can try this standalone example with a custom interceptor for network and server errors. These both will be handled differently in Retrofit2, so you have to check the returned error code from the server over the response code (response.code()) and if the response was not successful (!response.isSuccessful()).

For the case that the user has no connection to the network or the server you have to catch an IOException of the method Response response = chain.proceed(chain.request()); and handle the network error in the catch block.

HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
    loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    try {
                        Response response = chain.proceed(chain.request());
                        if (!response.isSuccessful()) {
                            Log.e("tag", "Failure central - response code: " + response.code());
                            Log.e("tag", "central server error handling");

                            // Central error handling for error responses here:
                            // e.g. 4XX and 5XX errors
                            switch (response.code()) {
                                case 401:
                                    // do something when 401 Unauthorized happened
                                    // e.g. delete credentials and forward to login screen
                                    // ...

                                    break;
                                case 403:
                                    // do something when 403 Forbidden happened
                                    // e.g. delete credentials and forward to login screen
                                    // ...

                                    break;
                                default:
                                    Log.e("tag", "Log error or do something else with error code:" + response.code());

                                    break;
                            }
                        }

                        return response;
                    } catch (IOException e) {
                        // Central error handling for network errors here:
                        // e.g. no connection to internet / to server

                        Log.e("tag", e.getMessage(), e);
                        Log.e("tag", "central network error handling");

                        throw e;
                    }
                }
            })
            .build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("http://10.0.2.2:8000/api/v1/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    UserRepository backendRepository = retrofit.create(UserRepository.class);
    backendRepository.getUser("userId123").enqueue(new Callback<UserModel>() {
        @Override
        public void onResponse(Call<UserModel> call, retrofit2.Response<UserModel> response) {
            Log.d("tag", "onResponse");

            if (!response.isSuccessful()) {
                Log.e("tag", "onFailure local server error handling code:" + response.code());
            } else {
                // its all fine with the request


            }
        }

        @Override
        public void onFailure(Call<UserModel> call, Throwable t) {
            Log.e("tag", "onFailure local network error handling");
            Log.e("tag", t.getMessage(), t);

        }
    });

UserRepository example:

public interface UserRepository {
    @GET("users/{userId}/")
    Call<UserModel> getUser(@Path("userId") String userId);

}

UserModel example:

public class UserModel implements Parcelable {
    @SerializedName("id")
    @Expose
    public String id = "";

    @SerializedName("email")
    @Expose
    public String mail = "";

    public UserModel() {

    }

    protected UserModel(Parcel in) {
        id = in.readString();
        mail = in.readString();
    }

    public static final Creator<UserModel> CREATOR = new Creator<UserModel>() {
        @Override
        public UserModel createFromParcel(Parcel in) {
            return new UserModel(in);
        }

        @Override
        public UserModel[] newArray(int size) {
            return new UserModel[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeString(mail);
    }
}

###

Fairly simply Retrofit custom error handling example. Is set up so that you don’t need to do much work in the ‘failure’ handler of a retrofit call to get the user-visible error message to show. Works on all endpoints. There’s lots of exception handling as our server folks like to keep us on our toes by sending all kinds of random stuff..!

// on error the server sends JSON
/*
{ "error": { "data": { "message":"A thing went wrong" } } }
*/
// create model classes..
public class ErrorResponse {
    Error error;

    public static class Error {
        Data data;
        public static class Data {
            String message;
        }
    }
}

//
/**
 * Converts the complex error structure into a single string you can get with error.getLocalizedMessage() in Retrofit error handlers.
 * Also deals with there being no network available
 *
 * Uses a few string IDs for user-visible error messages
 */
private static class CustomErrorHandler implements ErrorHandler {
    private final Context ctx;

    public CustomErrorHandler(Context ctx) {
        this.ctx = ctx;
    }

    @Override
    public Throwable handleError(RetrofitError cause) {
        String errorDescription;
        if (cause.isNetworkError()) {
            errorDescription = ctx.getString(R.string.error_network);
        } else {
            if (cause.getResponse() == null) {
                errorDescription = ctx.getString(R.string.error_no_response);
            } else {
// Error message handling - return a simple error to Retrofit handlers..
                try {
                    ErrorResponse errorResponse = (ErrorResponse) cause.getBodyAs(ErrorResponse.class);
                    errorDescription = errorResponse.error.data.message;
                } catch (Exception ex) {
                    try {
                        errorDescription = ctx.getString(R.string.error_network_http_error, cause.getResponse().getStatus());
                    } catch (Exception ex2) {
                        Log.e(TAG, "handleError: " + ex2.getLocalizedMessage());
                        errorDescription = ctx.getString(R.string.error_unknown);
                    }
                }
            }
        }
        return new Exception(errorDescription);
    }
}

// When creating the Server...
retrofit.RestAdapter restAdapter = new retrofit.RestAdapter.Builder()
        .setEndpoint(apiUrl)
        .setLogLevel(retrofit.RestAdapter.LogLevel.FULL)
        .setErrorHandler(new CustomErrorHandler(ctx)) // use error handler..
        .build();
server = restAdapter.create(Server.class);
// Now when calling server methods, get simple error out like this:
server.postSignIn(login,new Callback<HomePageResponse>(){

    @Override
    public void success(HomePageResponse homePageResponse,Response response){
    // Do success things!
    }

    @Override
    public void failure(RetrofitError error){
        error.getLocalizedMessage(); // <-- this is the message to show to user.
    }
});

Leave a Reply

Your email address will not be published. Required fields are marked *