Getting a new token when trying again to retry an old request with Volley

I have a simple authentication system implemented using Volley. It happens like this: Get a token from the server when you log in β†’ in an hour, this token expires β†’ when it expires, we will find it when the API call fails, so we must (when restarting) β†’ extract a new token when this call ends failure and then β†’ repeat the initial call.

I implemented this, and the token returns successfully, but since I think I'm doing something wrong with the Volley RequestQueue, the original request uses all of its retries before it can use the new and valid token. See the following code:

public class GeneralAPICall extends Request<JSONObject> { public static String LOG_TAG = GeneralAPICall.class.getSimpleName(); SessionManager sessionManager; //instance of sessionManager needed to get user credentials private Response.Listener<JSONObject> listener; //the response listener used to deliver the response private Map<String, String> headers = new HashMap<>(); //the headers used to authenticate private Map<String, String> params; //the params to pass with API call, can be null public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) { super(method, url, errorListener); sessionManager = new SessionManager(context); //instantiate HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user credentials for authentication this.listener = responseListener; this.params = params; //encode the user username and token String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + Constants.APIConstants.Characters.CHAR_COLON + credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP)); Log.v(LOG_TAG, loginEncoded); //TODO: remove this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header setRetryPolicy(new TokenRetryPolicy(context)); //**THE RETRY POLICY** } 

The installed retry policy is defined as the default, but I implement my own retry method as such:

 @Override public void retry(VolleyError error) throws VolleyError { Log.v(LOG_TAG, "Initiating a retry"); mCurrentRetryCount++; //increment our retry count mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); if (error instanceof AuthFailureError) { //we got a 401, and need a new token Log.v(LOG_TAG, "AuthFailureError found!"); VolleyUser.refreshTokenTask(context, this); //**GET A NEW TOKEN** } if (!hasAttemptRemaining()) { Log.v(LOG_TAG, "No attempt remaining, ERROR"); throw error; } } 

Update Token Task Defines RefreshAPICall

 public static void refreshTokenTask(Context context, IRefreshTokenReturn listener) { Log.v(LOG_TAG, "refresh token task called"); final IRefreshTokenReturn callBack = listener; RefreshAPICall request = new RefreshAPICall(Request.Method.GET, Constants.APIConstants.URL.GET_TOKEN_URL, context, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { try { String token = response.getString(Constants.APIConstants.Returns.RETURN_TOKEN); Log.v(LOG_TAG, "Token from return is: " + token); callBack.onTokenRefreshComplete(token); } catch (JSONException e) { callBack.onTokenRefreshComplete(null); //TODO: log this e.printStackTrace(); } } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.v(LOG_TAG, "Error with RETRY : " + error.toString()); } }); VolleySingleton.getInstance(context).addToRequestQueue(request); } 

The definition of our RefreshAPICall:

 public RefreshAPICall(int method, String url, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) { super(method, url, errorListener); sessionManager = new SessionManager(context); //instantiate HashMap<String, String> credentials = sessionManager.getRefreshUserDetails(); //get the user credentials for authentication this.listener = responseListener; //encode the user username and token String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + Constants.APIConstants.Characters.CHAR_COLON + credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD)).getBytes(), Base64.NO_WRAP)); this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //set the encoded information as the header setTag(Constants.VolleyConstants.RETRY_TAG); //mark the retry calls with a tag so we can delete any others once we get a new token setPriority(Priority.IMMEDIATE); //set priority as immediate because this needs to be done before anything else //debug lines Log.v(LOG_TAG, "RefreshAPICall made with " + credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + " " + credentials.get(Constants.SessionManagerConstants.KEY_PASSWORD)); Log.v(LOG_TAG, "Priority set on refresh call is " + getPriority()); Log.v(LOG_TAG, "Tag for Call is " + getTag()); } 

I set the priority of this request to high so that it fires to one that was unsuccessful, so as soon as we get the token, the original call can start with a valid token.

Finally, when I answer, I delete any other tasks with the retry tag (in the case of failure of several API calls and the execution of multiple retry calls, we do not want to rewrite the new token several times)

 @Override public void onTokenRefreshComplete(String token) { VolleySingleton.getInstance(context).getRequestQueue().cancelAll(Constants.VolleyConstants.RETRY_TAG); Log.v(LOG_TAG, "Cancelled all retry calls"); SessionManager sessionManager = new SessionManager(context); sessionManager.setStoredToken(token); Log.v(LOG_TAG, "Logged new token"); } 

Unfortunately, LogCat shows me that all attempts are made before we use the token. The token returns successfully, but it is obvious that IMMEDIATE priority does not affect the order in which the queue sends calls.

Any help on how to ensure that my RefreshAPICall is running before other tasks are greatly appreciated. I am wondering if Volley considers RefreshAPICall as a sub-task of the original unsuccessful task and therefore tries to call this original task for its number of repeaters until they exit, and then turns RefreshAPICall off.

LogCat (not sure how to make this look beautiful):

 05-05 16:12:07.145: E/Volley(1972): [137] BasicNetwork.performRequest: Unexpected response code **401 for https://url.me/api/get_friends** 05-05 16:12:07.145: V/TokenRetryPolicy(1972): Initiating a retry 05-05 16:12:07.145: V/TokenRetryPolicy(1972): AuthFailureError found! 05-05 16:12:07.146: V/VolleyUser(1972): refresh token task called 05-05 16:12:07.146: V/RefreshAPICall(1972): RefreshAPICall made with username user_password 05-05 16:12:07.147: V/RefreshAPICall(1972): Priority set on refresh call is HIGH 05-05 16:12:07.147: V/RefreshAPICall(1972): Tag for Call is retry 05-05 16:12:07.265: E/Volley(1972): [137] BasicNetwork.performRequest: Unexpected response code **401 for https://url.me/api/get_friends** 05-05 16:12:07.265: V/TokenRetryPolicy(1972): Initiating a retry 05-05 16:12:07.265: V/TokenRetryPolicy(1972): AuthFailureError found! 05-05 16:12:07.265: V/VolleyUser(1972): refresh token task called 05-05 16:12:07.265: V/RefreshAPICall(1972): RefreshAPICall made with user user_password 05-05 16:12:07.265: V/RefreshAPICall(1972): Priority set on refresh call is HIGH 05-05 16:12:07.265: V/RefreshAPICall(1972): Tag for Call is retry 05-05 16:12:07.265: V/TokenRetryPolicy(1972): No attempt remaining, ERROR 05-05 16:12:08.219: I/Choreographer(1972): Skipped 324 frames! The application may be doing too much work on its main thread. 05-05 16:12:08.230: V/RefreshAPICall(1972): Response from server on refresh is: {"status":"success","token":"d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64"} 05-05 16:12:08.230: V/VolleyUser(1972): Token from return is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64 05-05 16:12:08.231: V/TokenRetryPolicy(1972): Cancelled all retry calls 05-05 16:12:08.257: V/SessionManager(1972): New Token In SharedPref is: d5792e18c0e1acb3ad507dbae854eb2cdc5962a2c1b610a6b77e3bc3033c7f64 05-05 16:12:08.257: V/TokenRetryPolicy(1972): Logged new token 
+6
source share
2 answers

Sending a response now that I have found a semi-decent way to handle tokens on retry.

When I create my generic (most common) API call using Volley, I keep a link to the call in case of failure and pass it to my retry policy.

 public GeneralAPICall(int method, String url, Map<String, String> params, Context context, Response.Listener<JSONObject> responseListener, Response.ErrorListener errorListener) { super(method, url, errorListener); sessionManager = SessionManager.getmInstance(context); HashMap<String, String> credentials = sessionManager.getUserDetails(); // Get the user credentials for authentication this.listener = responseListener; this.params = params; // Encode the user username and token String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + Constants.APIConstants.Characters.CHAR_COLON + credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP)); this.headers.put(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); // Set the encoded information as the header setRetryPolicy(new TokenRetryPolicy(context, this)); //passing "this" saves the reference } 

Then in my retry policy class (which simply extends DefaultRetryPolicy, when I get a 401 error telling me that I need a new token, I cancel the refreshToken call to get a new one.

 public class TokenRetryPolicy extends DefaultRetryPolicy implements IRefreshTokenReturn{ ... @Override public void retry(VolleyError error) throws VolleyError { mCurrentRetryCount++; //increment our retry count mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); if (error instanceof AuthFailureError && sessionManager.isLoggedIn()) { mCurrentRetryCount = mMaxNumRetries + 1; // Don't retry anymore, it pointless VolleyUser.refreshTokenTask(context, this); // Get new token } if (!hasAttemptRemaining()) { Log.v(LOG_TAG, "No attempt remaining, ERROR"); throw error; } } ... } 

As soon as this call returns, I process the response in my retry policy class. I modify the called call by providing it with a new token (after saving the token in SharedPrefs) to authenticate myself, and then disconnect it again!

 @Override public void onTokenRefreshComplete(String token, String expiration) { sessionManager.setStoredToken(token, expiration); HashMap<String, String> credentials = sessionManager.getUserDetails(); //get the user credentials for authentication //encode the user username and token String loginEncoded = new String(Base64.encode((credentials.get(Constants.SessionManagerConstants.KEY_USERNAME) + Constants.APIConstants.Characters.CHAR_COLON + credentials.get(Constants.SessionManagerConstants.KEY_TOKEN)).getBytes(), Base64.NO_WRAP)); Log.v(LOG_TAG, loginEncoded); //TODO: remove callThatFailed.setHeaders(Constants.APIConstants.BasicAuth.AUTHORIZATION, Constants.APIConstants.BasicAuth.BASIC + loginEncoded); //modify "old, failed" call - set the encoded information as the header VolleySingleton.getInstance(context).getRequestQueue().add(callThatFailed); Log.v(LOG_TAG, "fired off new call"); } 

This implementation works great for me.

However , I should note that this situation should not happen because I found out that I should check if my token has expired before any API call makes. This is possible by storing the expiration time (returned from the server) in SharedPrefs and viewing if current_time is the expiration time <some_time and some_time is the time you would like to receive a new token before the expiration, for me 10 seconds.

Hope this helps someone out there, and if I'm wrong, please comment!

+8
source

I know this post, this old one, but posting my solution after the other proposed solutions did not help me.

Note. I tried the Brandon method mentioned above, i.e. the extension DefaultRetryPolicy. But the fields are private, so they don’t want to implement the whole class, there should be a better way.

So, I am writing code in the CustomRequest class extending Request. Here are the relevant snippets -

Save tokens in response -

 @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { ... //if oauth data is sent with response, store in SharedPrefs ... } 

If the access token has expired -

 @Override protected VolleyError parseNetworkError(VolleyError volleyError) { ... if (volleyError instanceof NoConnectionError) { //i know, there has to be a better way than checking this. //will work on it later if(volleyError.getMessage().equalsIgnoreCase("java.io.IOException: No authentication challenges found")) { String accessToken = getNewAccessToken();//synchronous call //retry if(accessToken != null) { //IMP: this is the statement which will retry the request manually NetworkHelper.get(mContext).getRequestQueue().add(this); } } } ... } 

Attach the access token to the request -

 @Override public Map<String, String> getHeaders() throws AuthFailureError { ... String accesssToken = //get from SharedPrefs headers.put("Authorization", "Bearer " +accessToken); ... } 

Go to the login screen if the update token is invalid -

 private void showLogin(){ //stop all current requests //cancelAllRequests(); Intent intent = new Intent(mContext, LoginActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); mContext.startActivity(intent); } 

Getting a new access token using an update token. This should be a synchronous method using RequestFuture -

 private String getNewAccessToken(){ ... //get new access token from server and store in SharedPrefs ... //also return the new token so that we know if we need to retry or not return newAccessToken; } 

NTN

0
source

Source: https://habr.com/ru/post/986601/


All Articles