Bootstrapper
Bootstrapper

Reputation: 1159

Retrofit 2.0 + RxJava + Error JSON body

I'm pretty new to RxJava and Retrofit and am trying to write my API calls with it. All the API calls return a JSON body on error which is in the general format as,

{"errors":[{"code":100, "message":"Login/Password not valid", "arguments":null}]}

Currently my code for the login API call (others are also similar) is,

mConnect.login(id, password)
        .subscribe(new Subscriber<Token>() {
            @Override
            public void onCompleted() {
                Log.d(TAG, "onCompleted()");
            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "onError(): " + e);
                if (e instanceof HttpException) {
                  // dump e.response().errorBody()
                }
            }

            @Override
            public void onNext(Token token) {
                Log.d(TAG, "onNext(): " + token);
            }
        });

When I get an error at the onError(), I would like to automatically decode the JSON in the error body to a POJO instead and use that. Is there a way to do this preferably in one place for all other API calls. Any help is appreciated.

Upvotes: 18

Views: 5855

Answers (2)

marius
marius

Reputation: 637

Deserialize may be an issue too. You can use the retrofit converter to deserialize it (or do it yourself).

My solution adds a bit to the one from murki:

<T> Observable.Transformer<T, T> parseHttpErrors() {
        return new Observable.Transformer<T, T>() {
            @Override
            public Observable<T> call(Observable<T> observable) {
                return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
                    @Override
                    public Observable<? extends T> call(Throwable throwable) {
                        if ( throwable instanceof HttpException ) {
                            Retrofit retrofit = new Retrofit.Builder()
                                    .baseUrl(SERVER_URL) // write your url here
                                    .addConverterFactory(GsonConverterFactory.create())
                                    .build();
                            Converter<ResponseBody, Error> errorConverter =
                                    retrofit.responseBodyConverter(Error.class, new Annotation[0]);
                            // Convert the error body into our Error type.
                            try {
                                Error error = errorConverter.convert(((HttpException) throwable).response().errorBody());
                                // Here you have two options, one is report this pojo back as error (onError() will be called),
                                return Observable.error(new Throwable(error.getMessage()));
                            }
                            catch (Exception e2) {
                                return Observable.error(new Throwable());
                            }

                        }
                        // if not the kind we're interested in, then just report the same error to onError()
                        return Observable.error(throwable);
                    }
                });
            }
        };
    }

and then at the onError(),

@Override
public void onError(Throwable e) {
    progressBar.setVisibility(View.GONE); // optional 
    if ( !TextUtils.isEmpty(e.getMessage()) ) {
            // show error as you like 
            return;
    }
    // show a default error if you wish
}

Upvotes: 0

murki
murki

Reputation: 879

I would suggest the use of a reusable Transformer along with the onErrorResumeNext operator to encapsulate your logic. It'd look something like this:

<T> Observable.Transformer<T, T> parseHttpErrors() {
    return new Observable.Transformer<T, T>() {
        @Override
        public Observable<T> call(Observable<T> observable) {
            return observable.onErrorResumeNext(new Func1<Throwable, Observable<? extends T>>() {
                @Override
                public Observable<? extends T> call(Throwable throwable) {
                    if (throwable instanceof HttpException) {
                        HttpErrorPojo errorPojo = // deserialize throwable.response().errorBody();

                        // Here you have two options, one is report this pojo back as error (onError() will be called),
                        return Observable.error(errorPojo); // in this case HttpErrorPojo would need to inherit from Throwable

                        // or report this pojo back as part of onNext()
                        return Observable.just(errorPojo); //in this case HttpErrorPojo would need to inherit from <T>
                    }
                    // if not the kind we're interested in, then just report the same error to onError()
                    return Observable.error(throwable);
                }
            });
        }
    };
}

Pay attention to the comments in the code, since you have to make the decision whether you want to report the parsed response onError() or onNext().

Then you can use this transformer anywhere in your API calls like this:

mConnect.login(id, password)
        .compose(this.<Token>parseHttpErrors()) // <-- HERE
        .subscribe(new Subscriber<Token>() {
            @Override
            public void onCompleted() {
                Log.d(TAG, "onCompleted()");
            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "onError(): " + e);
                if (e instanceof HttpErrorPojo) {
                  // this will be called if errorPojo was reported via Observable.error()
                }
            }

            @Override
            public void onNext(Token token) {
                Log.d(TAG, "onNext(): " + token);
                if (token instanceof HttpErrorPojo) {
                  // this will be called if errorPojo was reported via Observable.just()
                }
            }
        });

Upvotes: 6

Related Questions