Retrofit: Redirect to LoginActivity if response code is 401
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.
-
Create event model
public class UnauthorizedEvent { private static final UnauthorizedEvent INSTANCE = new UnauthorizedEvent(); public static UnauthorizedEvent instance() { return INSTANCE; } private UnauthorizedEvent() { } }
-
Implement custom
Interceptor
which disptaches event about unauthorized reqeustsclass 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; } }
-
Create
BaseActivity
class which handlesUnauthorizedEvent
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); } }
-
Prevent launching
LoginActivity
fromLoginActivity
public class LoginActivty extends BaseActivity { @Override protected void handleUnauthorizedEvent() { //Don't handle unauthorized event } }
Another approach is to not extending
BaseActivity
here. -
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 ofBaseCallback
)
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
}
Rick
Updated on June 06, 2022Comments
-
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) { } });