Retrofit: Redirect to LoginActivity if response code is 401

14,589

Solution 1

Personally, I would like to suggest using event bus pattern here. You can use greenrobot implementation or whatever you want, since it's more about an architecture approach rather than concrete implementation.

  1. Create event model

    public class UnauthorizedEvent {
    
        private static final UnauthorizedEvent INSTANCE = new UnauthorizedEvent();
    
        public static UnauthorizedEvent instance() {
            return INSTANCE;
        }
    
        private UnauthorizedEvent() {
        }
    }
    
  2. Implement custom Interceptor which disptaches event about unauthorized reqeusts

    class UnauthorizedInterceptor implements Interceptor {
    
        @Override
        public Response intercept(@NonNull Chain chain) throws IOException {
            Response response = chain.proceed(chain.request());
            if (response.code() == 401) {
                EventBus.getDefault().post(UnauthorizedEvent.instance());
            }
            return response;
        }
    }
    
  3. Create BaseActivity class which handles UnauthorizedEvent

    public class BaseActivity extends Activity {
    
        @Override
        public void onStart() {
            super.onStart();
            EventBus.getDefault().register(this);
        }
    
        @Override
        public void onStop() {
            super.onStop();
            EventBus.getDefault().unregister(this);
        }
    
        @Subscribe
        public final void onUnauthorizedEvent(UnauthorizedEvent e) {
            handleUnauthorizedEvent();
        }
    
        protected void handleUnauthorizedEvent() {
            Intent intent = new Intent(this, LoginActivity.class);
            startActivity(intent);
        }
    }
    
  4. Prevent launching LoginActivity from LoginActivity

    public class LoginActivty extends BaseActivity {
    
        @Override
        protected void handleUnauthorizedEvent() {
            //Don't handle unauthorized event
        }
    }
    

    Another approach is to not extending BaseActivity here.

  5. Register your interceptor

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new UnauthorizedInterceptor())
            .build();
    

Pros:

  • Loose coupling between components
  • Easaly extending the logic by overriding handleUnauthorizedEvent
  • No need to rewrite code to use new types of callbacks
  • Reduce human factor about making mistakes (using Callback instead of BaseCallback)

Cons:

  • EventBus pattern makes debugging more complicated
  • One more dependency or own implementation which brings new code to the project

Also, please take into account, that this example doesn't cover multithreading issues. It solves your problem of handling unauthorized requests. Thus, if two requests receive 401 than it is possible that 2 instances of LoginActivity is started.

Solution 2

Consider introducing a custom implementation of retrofit2.Callback interface, e.g. BaseCallback:

public abstract class BaseCallback<T> implements Callback<T> {

    private final Context context;

    public BaseCallback(Context context) {
        this.context = context;
    }

    @Override
    public void onResponse(Call<T> call, Response<T> response) {
        if (response.code() == 401) {
            // launch login activity using `this.context`
        } else {
            onSuccess(response.body());
        }
    }

    @Override
    public void onFailure(Call<T> call, Throwable t) {

    }

    abstract void onSuccess(T response);

}

Now, from the caller site you should change new Callback<Token> with new BaseCallback<Token>:

call.enqueue(new BaseCallback<Token>(context) {
    @Override
    void onSuccess(Token response) {
        // do something with token
    }
});

Although, this approach doesn't fulfil your following statement:

so I don't have to keep repeat the same code over again for each api call

nevertheless, I cannot come up with a better approach.

Solution 3

The simplest way is to inject activity context in Interceptor instance. If you are using some DI tools, like Dagger2 or Toothpick it will be very simple. I recommend to use toothpick)

https://github.com/stephanenicolas/toothpick

The most code near by will be in kotlin, because it's my boilerplate code. Those thinks, that you are need to solve your problem i will write in Java.

The solution will be like this:

@Qualifier
annotation class BackendUrl


class ActivityModule(activity: BaseActivity): Module() {

    init {
        bind(Activity::class.java).toInstance(activity)
    }

}

class NetworkModule: Module() {

    init {
        bind(String::class.java).withName(BackendUrl::class.java).toInstance(Constants.URL)
        bind(Gson::class.java).toInstance(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create())
        bind(CacheHolder::class.java).toProvider(CacheProvider::class.java).singletonInScope()
        bind(OkHttpClient::class.java).toProvider(OkHttpProvider::class.java).instancesInScope()
        bind(BackendApi::class.java).toProvider(BackendApiProvider::class.java).instancesInScope()
        bind(RedirectInterceptor::class.java).to(RedirectInterceptor::class.java)
    }

}

Than you need to create Providers for injection dependency

class BackendApiProvider @Inject constructor(
        private val okHttpClient: OkHttpClient,
        private val gson: Gson,
        @BackendUrl private val serverPath: String
) : Provider<BackendApi> {

    override fun get() =
            Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .client(okHttpClient)
                    .baseUrl(serverPath)
                    .build()
                    .create(BackendApi::class.java)
}

And your redirect interceptor:

public class RedirectInterceptor implements Interceptor {

    private final Context context;

    @Inject
    public RedirectInterceptor(Activity context) {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request newRequest = chain.request().newBuilder()
                .build();

        Response response =  chain.proceed(newRequest);
        Log.d("MyApp", "Code : "+response.code());
        if (response.code() == 401){
            Intent intent = new Intent(context, LoginActivity.class);
            context.startActivity(intent);
            ((Activity) context).finish();
            return response;
        }

        return chain.proceed(newRequest);
    }
}

Oh, yes. For Authorization header will be better to create new instance of another interceptor

class HeaderInterceptor(private val token: String?) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val newRequest =  request.newBuilder()

        Log.d(TAG, "token: $token")

        if (token != null && token.isNotBlank()) {
            newRequest.addHeader("Authorization", "Bearer $token")
        }

        return chain.proceed(newRequest.build())
    }

    companion object {
        private val TAG = HeaderInterceptor::class.java.toString()
    }

} 

And your OkhttpProvder

class OkHttpProvider @Inject constructor(cacheHolder: CacheHolder, prefs: IPreferences, redirectInterceptor: RedirectInterceptor) : Provider<OkHttpClient> {

    private val client: OkHttpClient

    init {

        val builder = OkHttpClient.Builder()
        builder
                .addNetworkInterceptor(redirectInterceptor)
                .addNetworkInterceptor(HeaderInterceptor(prefs.getAuthToken()))
                .readTimeout(30, TimeUnit.SECONDS)
                .cache(cacheHolder.okHttpCache)

        client = builder.build()
    }

    override fun get() = client
}

Thats all! Now, you just only need to place you modules in right scopes.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.init_view)      

        Toothpick.openScopes("activity scope").apply {
            installModules(ActivityModule(this@YourActivity))
            Toothpick.inject(this@YourActivity, this)
        }

        Toothpick.openScopes("activity scope", "network scope").apply {
            installModules(NetworkModule())
        }

        // your activity code
    }

Solution 4

Generalized Solution: You can solve it by generalizing the error handling. You can use custom CallAdapterFactory to the Retrofit builder. Please refer below classes :

RxErrorHandlingCallAdapterFactory :

public class RxErrorHandlingCallAdapterFactory extends CallAdapter.Factory {
    private static Context mContext = null;
    private final RxJava2CallAdapterFactory original;

    private RxErrorHandlingCallAdapterFactory() {
        original = RxJava2CallAdapterFactory.create();
    }

    public static CallAdapter.Factory create(Context context) {
        mContext = context;
        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<R> implements CallAdapter<R, Object> {
        private final Retrofit retrofit;
        private final CallAdapter<R,
                Object> wrapped;

        public RxCallAdapterWrapper(Retrofit retrofit, CallAdapter<R, Object> wrapped) {
            this.retrofit = retrofit;
            this.wrapped = wrapped;
        }

        @Override
        public Type responseType() {
            return wrapped.responseType();
        }

        @Override
        public Object adapt(Call<R> call) {
            Object result = wrapped.adapt(call);
            if (result instanceof Single) {
                return ((Single) result).onErrorResumeNext(new Function<Throwable, SingleSource>() {
                    @Override
                    public SingleSource apply(@NonNull Throwable throwable) throws Exception {
                        return Single.error(asRetrofitException(throwable));
                    }
                });
            }
            if (result instanceof Observable) {
                return ((Observable) result).onErrorResumeNext(new Function<Throwable, ObservableSource>() {
                    @Override
                    public ObservableSource apply(@NonNull Throwable throwable) throws Exception {
                        return Observable.error(asRetrofitException(throwable));
                    }
                });
            }
            if (result instanceof Completable) {
                return ((Completable) result).onErrorResumeNext(new Function<Throwable, CompletableSource>() {
                    @Override
                    public CompletableSource apply(@NonNull Throwable throwable) throws Exception {
                        return Completable.error(asRetrofitException(throwable));
                    }
                });
            }
            return result;
        }

        private RetrofitException asRetrofitException(Throwable throwable) {
            // We had non-200 http error
            Log.v("log", "eror");
            throwable.printStackTrace();
            if (throwable instanceof HttpException) {
                HttpException httpException = (HttpException) throwable;
                final Response response = httpException.response();


                //if ((mContext instanceof Activity)) {

                String s = "Something went wrong."; //mContext.getString(R.string.something_went_wrong);
                try {
                    s = new JSONObject(response.errorBody().string()).getString("message");
                    if (response.code() == 401) { // 401 Unauthorized
                        Intent intent = new Intent(mContext, LoginActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                        mContext.startActivity(intent);
                    }
                } catch (JSONException | IOException e) {
                    e.printStackTrace();
                }

                return RetrofitException.unexpectedError(s, response, retrofit);

                //showErrorDialog(mContext, response);
                //}

//                return RetrofitException.httpError(response.errorBody().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);
        }
    }
}

RetrofitException :

public class RetrofitException extends RuntimeException {
    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;
    }

    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);
    }

    public static RetrofitException unexpectedError(String s, Response response, Retrofit retrofit) {
        return new RetrofitException(s, null, null, Kind.UNEXPECTED, null, null);
    }

    /**
     * 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.responseBodyConverter(type, new Annotation[0]);
        return converter.convert(response.errorBody());
    }

    /**
     * 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
    }
}

Retrofit Builder :

Retrofit retrofit = new Retrofit.Builder()
            .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create(context))
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(API_URL)
                .client(client)
                .build();

You can handle 401 in RxErrorHandlingCallAdapterFactory and other errors through Throwable.

Solution 5

This is how interceptor worked for handling 401 globally

public class ResponseHeaderInterceptor implements Interceptor {
private final Context context;

public ResponseHeaderInterceptor(Context context) {
    this.context = context;
}

@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
    Response response = chain.proceed(chain.request());
    if(response.code() == 401){
        SharedPreferences pref = context.getSharedPreferences(Constants.PREFERENCES, 0);
        String userName = pref.getString("key_user_email", "");
        //clear shared preferences
        pref.edit().clear().apply();
        Bundle params = new Bundle();
        params.putString("user", userName);
        FirebaseAnalytics.getInstance(context).logEvent(Constants.USER_UNAUTHORIZED_EVENT, params);
        Intent intent = new Intent(this.context, IntroActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        this.context.startActivity(intent);
    }
    return response;
}

}

adding to okhttp client of retrofit

var okHttpClient: OkHttpClient = OkHttpClient()
            .newBuilder()
            .addInterceptor(ResponseHeaderInterceptor(MyApplication.getMyApplicationContext()))//Header interceptor for logging network responses
            .build()
    private var retrofit: Retrofit? = null
    val client: Retrofit?
        get() {
            if (retrofit == null) {
                retrofit = Retrofit.Builder()
                        .client(okHttpClient)
                        .baseUrl(BuildConfig.SERVER)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build()
            }
            return retrofit
        }
Share:
14,589
Rick
Author by

Rick

Updated on June 06, 2022

Comments

  • Rick
    Rick almost 2 years

    How to start LoginActivity from the interceptor(non-activity class)? I have tried the code (Interceptor) below but not working for me.

    Interceptor

    OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
    
                    Request newRequest = chain.request().newBuilder()
                            .addHeader("Authorization", "Bearer " + auth_token_string)
                            .build();
    
                    Response response =  chain.proceed(newRequest);
                    Log.d("MyApp", "Code : "+response.code());
                    if (response.code() == 401){
                        Intent intent = new Intent(SplashActivity.getContextOfApplication(), LoginActivity.class);
                        startActivity(intent);
                        finish();  //Not working
                        return response;
                    }
    
                    return chain.proceed(newRequest);
                }
            }).build();
    

    This is the current solution I'm using, is there any better solution than this? This solution has to keep repeat on every api call.

    MainActivity

    call.enqueue(new Callback<Token>() {
                @Override
                public void onResponse(Call<Token> call, Response<Token> response) {
                    if(response.isSuccessful())
                    {
                        //success
                    }
                    else
                    {
                        Intent intent = new Intent(MainActivity.this.getApplicationContext(), LoginActivity.class);
                        startActivity(intent);
                        finish();
                    }
                }
                @Override
                public void onFailure(Call<Token> call, Throwable t) {
    
                }
            });