Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrofit/Rxjava and session-based services

I'm implementing session-based services. All requests have to be subscribed with a cookie session param, which in turn is retrieved with separate rest api. So the basic workflow would be to get the session cookie and proceed querying the services. Occasionally the cookie would expire and it would lead to another session cookie request.

I'm trying to make client code session-agnostic, so that it doesn't have to worry about maintaining session, but rather I want it to be hidden inside services layer.

Can you suggest ideas on implementing it with Retrofit/RxJava? I think the SessionService has to be encapsulated by all other services, so that they can query it whenever it's required but I'm not sure how to do it with Retrofit's RestAdapter.create

like image 641
midnight Avatar asked Aug 28 '14 10:08

midnight


People also ask

What is the difference between RxJava and retrofit?

Rx gives you a very granular control over which threads will be used to perform work in various points within a stream. To point the contrast here already, basic call approach used in Retrofit is only scheduling work on its worker threads and forwarding the result back into the calling thread.

What is RxJava in Android example?

RxJava is a JVM library that uses observable sequences to perform asynchronous and event-based programming. Its primary building blocks are triple O's, which stand for Operator, Observer, and Observables. And we use them to complete asynchronous tasks in our project. It greatly simplifies multithreading in our project.


1 Answers

I've done something similar to this before but with OAuth authorization. Basically, you have a RestAdapter initialized with a RequestInterceptor that adds the session cookie to each request. The RequestInterceptor gets a new session cookie any time a session is authorized.

The following Retrofit REST interface definition is used in the example code below:

interface ApiService {
    @GET("/examples/v1/example")
    Observable<Example> getExample();
}

The Request interceptor gets a peek at each REST request and can add headers, query params or can modify the URL. This example assumes the cookie is added as an HTTP header.

class CookieHeaderProvider implements RequestInterceptor {
    private String sessionCookie = "";

    public CookieHeaderProvider() {
    }

    public void setSesstionCookie(String sessionCookie) {
        this.sessionCookie = sessionCookie;
    }

    @Override
    public void intercept(RequestFacade requestFacade) {
        requestFacade.addHeader("Set-Cookie", sessionCookie);
    }
}

This is the SessionService you alluded to. It's responsibility is to make the network request that authorizes/refreshes the session cookie.

class SessionService {
    // Modify contructor params to pass in anything needed 
    // to get the session cookie.
    SessionService(...) {
    }

    public Observable<String> observeSessionCookie(...) {
        // Modify to return an Observable that when subscribed to
        // will make the network request to get the session cookie.
        return Observable.just("Fake Session Cookie");
    }
}

The RestService class wraps the Retrofit interface so that request retry logic can be added to each Retrofit Observable.

class RestService {
    private final apiService;
    private final sessionSerivce;
    private final cookieHeaderProvider;

    RestService(ApiService apiService, 
                SessionService sessionSerivce,
                CookieHeaderProvider cookieHeaderProvider) {
        this.apiService = apiService;
        this.sessionSerivce = sessionSerivce;
        this.cookieHeaderProvider = cookieHeaderProvider;
    }

    Observable<Example> observeExamples() {
        // Return a Retrofit Observable modified with
        // session retry logic.
        return 
            apiService
                .observeExamples()
                .retryWhen(new RetryWithSessionRefresh(sessionSerivce, cookieHeaderProvider)); 
    }
}

The retry logic below will use the SessionService to update the session cookie and then retry failed REST requests if the session cookie sent to the server returns an HTTP Unauthorized (401) error.

public class RetryWithSessionRefresh implements
        Func1<Observable<? extends Throwable>, Observable<?>> {

    private final SessionService sessionSerivce;
    private final CookieHeaderProvider cookieHeaderProvider;

    public RetryWithSessionRefresh(SessionService sessionSerivce,
                                   CookieHeaderProvider cookieHeaderProvider) {
        this.sessionSerivce = sessionSerivce;
        this.cookieHeaderProvider = cookieHeaderProvider;
    }

    @Override
    public Observable<?> call(Observable<? extends Throwable> attempts) {
        return attempts
                .flatMap(new Func1<Throwable, Observable<?>>() {
                    public int retryCount = 0;

                    @Override
                    public Observable<?> call(final Throwable throwable) {
                        // Modify retry conditions to suit your needs. The following
                        // will retry 1 time if the error returned was an
                        // HTTP Unauthoried (401) response.
                        retryCount++;
                        if (retryCount <= 1 && throwable instanceof RetrofitError) {
                            final RetrofitError retrofitError = (RetrofitError) throwable;
                            if (!retrofitError.isNetworkError()
                                    && retrofitError.getResponse().getStatus() == HttpStatus.SC_UNAUTHORIZED) {
                                return sessionSerivce
                                        .observeSessionCookie()
                                        .doOnNext(new Action1<String>() {
                                            @Override
                                            public void call(String sessionCookie) {
                                                // Update session cookie so that next
                                                // retrofit request will use it.
                                                cookieHeaderProvider.setSesstionCookie(sessionCookie);
                                            }
                                        })
                                        .doOnError(new Action1<Throwable>() {
                                            @Override
                                            public void call(Throwable throwable) {
                                                // Clear session cookie on error.
                                                cookieHeaderProvider.setSesstionCookie("");
                                            }
                                        });
                            }
                        }
                        // No more retries. Pass the original
                        // Retrofit error through.
                        return Observable.error(throwable);
                    }
                });
    }
}

Client initialization code will look similar to this:

CookieHeaderProvider cookieHeaderProvider = new CookieHeaderProvider();
SessionService sessionSerivce = new SessionService();

ApiService apiService =
    new RestAdapter.Builder()
        .setEndpoint(...)
        .setClient(...)
        .setRequestInterceptor(cookieHeaderProvider)
        .build()
        .create(ApiService.class);

RestService restService =
    new RestService(apiService, sessionSerivce, cookieHeaderProvider);

Then get a REST observable from the RestService and subscribe to it to kick off the network request.

Observable<Example> exampleObservable =
    restService
        .observeExamples();

Subsctiption subscription =
    exampleObservable
        .subscribe(new Observer<Example>() {
            void onNext(Example example) {
                // Do stuff with example
            }
            void onCompleted() {
                // All done.
            }
            void onError(Throwalbe e) {
                // All API errors will end up here.
            }
        });
like image 163
kjones Avatar answered Nov 15 '22 14:11

kjones