Skip to content

Commit 4bed13d

Browse files
authored
Merge pull request #656 from Iterable/evan/MOB-7154-jwt-retry-omni
[MOB-7154] Implement JWT Retry Logic for Android SDK
2 parents 7ee5748 + 7a8d798 commit 4bed13d

File tree

3 files changed

+199
-61
lines changed

3 files changed

+199
-61
lines changed

iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java

+68
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import androidx.test.filters.MediumTest;
88

99
import org.hamcrest.CoreMatchers;
10+
import org.json.JSONException;
1011
import org.json.JSONObject;
1112
import org.junit.After;
1213
import org.junit.Before;
@@ -25,6 +26,7 @@
2526
import static com.iterable.iterableapi.IterableTestUtils.createIterableApi;
2627
import static junit.framework.Assert.assertEquals;
2728
import static junit.framework.Assert.assertNotNull;
29+
import static junit.framework.Assert.assertNull;
2830
import static junit.framework.Assert.assertTrue;
2931
import static org.junit.Assert.assertThat;
3032

@@ -198,6 +200,72 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
198200
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
199201
}
200202

203+
@Test
204+
public void testRetryOnInvalidJwtPayload() throws Exception {
205+
final CountDownLatch signal = new CountDownLatch(3);
206+
stubAnyRequestReturningStatusCode(401, "{\"msg\":\"JWT Authorization header error\",\"code\":\"InvalidJwtPayload\"}");
207+
208+
IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, new IterableHelper.FailureHandler() {
209+
@Override
210+
public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
211+
try {
212+
if (data != null && "InvalidJwtPayload".equals(data.optString("code"))) {
213+
final JSONObject responseData = new JSONObject("{\n" +
214+
" \"key\":\"Success\",\n" +
215+
" \"message\":\"Event tracked successfully.\"\n" +
216+
"}");
217+
stubAnyRequestReturningStatusCode(200, responseData);
218+
219+
new IterableRequestTask().execute(new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, new IterableHelper.SuccessHandler() {
220+
@Override
221+
public void onSuccess(@NonNull JSONObject successData) {
222+
try {
223+
assertEquals(responseData.toString(), successData.toString());
224+
} catch (AssertionError e) {
225+
e.printStackTrace();
226+
} finally {
227+
signal.countDown();
228+
}
229+
}
230+
}, null));
231+
server.takeRequest(2, TimeUnit.SECONDS);
232+
}
233+
} catch (JSONException e) {
234+
e.printStackTrace();
235+
} catch (Exception e) {
236+
e.printStackTrace();
237+
} finally {
238+
signal.countDown();
239+
}
240+
}
241+
});
242+
243+
new IterableRequestTask().execute(request);
244+
server.takeRequest(1, TimeUnit.SECONDS);
245+
246+
// Await for the background tasks to complete
247+
signal.await(5, TimeUnit.SECONDS);
248+
}
249+
250+
@Test
251+
public void testMaxRetriesOnMultipleInvalidJwtPayloads() throws Exception {
252+
for (int i = 0; i < 5; i++) {
253+
stubAnyRequestReturningStatusCode(401, "{\"msg\":\"JWT Authorization header error\",\"code\":\"InvalidJwtPayload\"}");
254+
}
255+
256+
IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null);
257+
IterableRequestTask task = new IterableRequestTask();
258+
task.execute(request);
259+
260+
RecordedRequest request1 = server.takeRequest(1, TimeUnit.SECONDS);
261+
RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS);
262+
RecordedRequest request3 = server.takeRequest(5, TimeUnit.SECONDS);
263+
RecordedRequest request4 = server.takeRequest(5, TimeUnit.SECONDS);
264+
RecordedRequest request5 = server.takeRequest(5, TimeUnit.SECONDS);
265+
RecordedRequest request6 = server.takeRequest(5, TimeUnit.SECONDS);
266+
assertNull("Request should be null since retries hit the max of 5", request6);
267+
}
268+
201269
@Test
202270
public void testResponseCode500() throws Exception {
203271
for (int i = 0; i < 5; i++) {

iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java

+57-33
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
import androidx.annotation.VisibleForTesting;
66

7-
import com.iterable.iterableapi.util.Future;
8-
7+
import org.json.JSONException;
98
import org.json.JSONObject;
109

1110
import java.io.UnsupportedEncodingException;
1211
import java.util.Timer;
1312
import java.util.TimerTask;
14-
import java.util.concurrent.Callable;
13+
import java.util.concurrent.ExecutorService;
14+
import java.util.concurrent.Executors;
1515

1616
public class IterableAuthManager {
1717
private static final String TAG = "IterableAuth";
@@ -20,55 +20,53 @@ public class IterableAuthManager {
2020
private final IterableApi api;
2121
private final IterableAuthHandler authHandler;
2222
private final long expiringAuthTokenRefreshPeriod;
23-
23+
private final long scheduledRefreshPeriod = 10000;
2424
@VisibleForTesting
2525
Timer timer;
2626
private boolean hasFailedPriorAuth;
2727
private boolean pendingAuth;
2828
private boolean requiresAuthRefresh;
2929

30+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
31+
3032
IterableAuthManager(IterableApi api, IterableAuthHandler authHandler, long expiringAuthTokenRefreshPeriod) {
3133
this.api = api;
3234
this.authHandler = authHandler;
3335
this.expiringAuthTokenRefreshPeriod = expiringAuthTokenRefreshPeriod;
3436
}
3537

3638
public synchronized void requestNewAuthToken(boolean hasFailedPriorAuth) {
39+
requestNewAuthToken(hasFailedPriorAuth, null);
40+
}
41+
42+
private void handleSuccessForAuthToken(String authToken, IterableHelper.SuccessHandler successCallback) {
43+
try {
44+
JSONObject object = new JSONObject();
45+
object.put("newAuthToken", authToken);
46+
successCallback.onSuccess(object);
47+
} catch (JSONException e) {
48+
e.printStackTrace();
49+
}
50+
}
51+
52+
public synchronized void requestNewAuthToken(
53+
boolean hasFailedPriorAuth,
54+
final IterableHelper.SuccessHandler successCallback) {
3755
if (authHandler != null) {
3856
if (!pendingAuth) {
3957
if (!(this.hasFailedPriorAuth && hasFailedPriorAuth)) {
4058
this.hasFailedPriorAuth = hasFailedPriorAuth;
4159
pendingAuth = true;
42-
Future.runAsync(new Callable<String>() {
43-
@Override
44-
public String call() throws Exception {
45-
return authHandler.onAuthTokenRequested();
46-
}
47-
}).onSuccess(new Future.SuccessCallback<String>() {
60+
61+
executor.submit(new Runnable() {
4862
@Override
49-
public void onSuccess(String authToken) {
50-
if (authToken != null) {
51-
queueExpirationRefresh(authToken);
52-
} else {
53-
IterableLogger.w(TAG, "Auth token received as null. Calling the handler in 10 seconds");
54-
//TODO: Make this time configurable and in sync with SDK initialization flow for auth null scenario
55-
scheduleAuthTokenRefresh(10000);
56-
authHandler.onTokenRegistrationFailed(new Throwable("Auth token null"));
57-
return;
63+
public void run() {
64+
try {
65+
final String authToken = authHandler.onAuthTokenRequested();
66+
handleAuthTokenSuccess(authToken, successCallback);
67+
} catch (final Exception e) {
68+
handleAuthTokenFailure(e);
5869
}
59-
IterableApi.getInstance().setAuthToken(authToken);
60-
pendingAuth = false;
61-
reSyncAuth();
62-
authHandler.onTokenRegistrationSuccessful(authToken);
63-
}
64-
})
65-
.onFailure(new Future.FailureCallback() {
66-
@Override
67-
public void onFailure(Throwable throwable) {
68-
IterableLogger.e(TAG, "Error while requesting Auth Token", throwable);
69-
authHandler.onTokenRegistrationFailed(throwable);
70-
pendingAuth = false;
71-
reSyncAuth();
7270
}
7371
});
7472
}
@@ -82,6 +80,32 @@ public void onFailure(Throwable throwable) {
8280
}
8381
}
8482

83+
private void handleAuthTokenSuccess(String authToken, IterableHelper.SuccessHandler successCallback) {
84+
if (authToken != null) {
85+
if (successCallback != null) {
86+
handleSuccessForAuthToken(authToken, successCallback);
87+
}
88+
queueExpirationRefresh(authToken);
89+
} else {
90+
IterableLogger.w(TAG, "Auth token received as null. Calling the handler in 10 seconds");
91+
//TODO: Make this time configurable and in sync with SDK initialization flow for auth null scenario
92+
scheduleAuthTokenRefresh(scheduledRefreshPeriod);
93+
authHandler.onTokenRegistrationFailed(new Throwable("Auth token null"));
94+
return;
95+
}
96+
IterableApi.getInstance().setAuthToken(authToken);
97+
pendingAuth = false;
98+
reSyncAuth();
99+
authHandler.onTokenRegistrationSuccessful(authToken);
100+
}
101+
102+
private void handleAuthTokenFailure(Throwable throwable) {
103+
IterableLogger.e(TAG, "Error while requesting Auth Token", throwable);
104+
authHandler.onTokenRegistrationFailed(throwable);
105+
pendingAuth = false;
106+
reSyncAuth();
107+
}
108+
85109
public void queueExpirationRefresh(String encodedJWT) {
86110
clearRefreshTimer();
87111
try {
@@ -96,7 +120,7 @@ public void queueExpirationRefresh(String encodedJWT) {
96120
IterableLogger.e(TAG, "Error while parsing JWT for the expiration", e);
97121
authHandler.onTokenRegistrationFailed(new Throwable("Auth token decode failure. Scheduling auth token refresh in 10 seconds..."));
98122
//TODO: Sync with configured time duration once feature is available.
99-
scheduleAuthTokenRefresh(10000);
123+
scheduleAuthTokenRefresh(scheduledRefreshPeriod);
100124
}
101125
}
102126

iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java

+74-28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import android.net.Uri;
44
import android.os.AsyncTask;
55
import android.os.Handler;
6+
import android.os.Looper;
7+
68
import androidx.annotation.NonNull;
79
import androidx.annotation.Nullable;
810
import androidx.annotation.WorkerThread;
@@ -38,11 +40,13 @@ class IterableRequestTask extends AsyncTask<IterableApiRequest, Void, IterableAp
3840
static final String ERROR_CODE_INVALID_JWT_PAYLOAD = "InvalidJwtPayload";
3941

4042
int retryCount = 0;
43+
boolean shouldRetryWhileJwtInvalid = true;
4144
IterableApiRequest iterableApiRequest;
4245

4346
/**
4447
* Sends the given request to Iterable using a HttpUserConnection
4548
* Reference - http://developer.android.com/reference/java/net/HttpURLConnection.html
49+
*
4650
* @param params
4751
* @return
4852
*/
@@ -54,6 +58,23 @@ protected IterableApiResponse doInBackground(IterableApiRequest... params) {
5458
return executeApiRequest(iterableApiRequest);
5559
}
5660

61+
public void setShouldRetryWhileJwtInvalid(boolean shouldRetryWhileJwtInvalid) {
62+
this.shouldRetryWhileJwtInvalid = shouldRetryWhileJwtInvalid;
63+
}
64+
65+
private void retryRequestWithNewAuthToken(String newAuthToken) {
66+
IterableApiRequest request = new IterableApiRequest(
67+
iterableApiRequest.apiKey,
68+
iterableApiRequest.resourcePath,
69+
iterableApiRequest.json,
70+
iterableApiRequest.requestType,
71+
newAuthToken,
72+
iterableApiRequest.legacyCallback);
73+
IterableRequestTask requestTask = new IterableRequestTask();
74+
requestTask.setShouldRetryWhileJwtInvalid(false);
75+
requestTask.execute(request);
76+
}
77+
5778
@WorkerThread
5879
static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiRequest) {
5980
IterableApiResponse apiResponse = null;
@@ -269,50 +290,75 @@ private static boolean isSensitive(String key) {
269290
return (key.equals(IterableConstants.HEADER_API_KEY)) || key.equals(IterableConstants.HEADER_SDK_AUTHORIZATION);
270291
}
271292

293+
private static final Handler handler = new Handler(Looper.getMainLooper());
294+
272295
@Override
273296
protected void onPostExecute(IterableApiResponse response) {
274-
boolean retryRequest = !response.success && response.responseCode >= 500;
275-
276-
if (retryRequest && retryCount <= MAX_RETRY_COUNT) {
277-
final IterableRequestTask requestTask = new IterableRequestTask();
278-
requestTask.setRetryCount(retryCount + 1);
279-
280-
long delay = 0;
281-
if (retryCount > 2) {
282-
delay = RETRY_DELAY_MS * retryCount;
283-
}
284297

285-
Handler handler = new Handler();
286-
handler.postDelayed(new Runnable() {
287-
@Override
288-
public void run() {
289-
requestTask.execute(iterableApiRequest);
290-
}
291-
}, delay);
298+
if (shouldRetry(response)) {
299+
retryRequestWithDelay();
292300
return;
293301
} else if (response.success) {
294-
IterableApi.getInstance().getAuthManager().resetFailedAuth();
295-
if (iterableApiRequest.successCallback != null) {
296-
iterableApiRequest.successCallback.onSuccess(response.responseJson);
297-
}
302+
handleSuccessResponse(response);
298303
} else {
299-
if (matchesErrorCode(response.responseJson, ERROR_CODE_INVALID_JWT_PAYLOAD)) {
300-
IterableApi.getInstance().getAuthManager().requestNewAuthToken(true);
301-
}
302-
if (iterableApiRequest.failureCallback != null) {
303-
iterableApiRequest.failureCallback.onFailure(response.errorMessage, response.responseJson);
304-
}
304+
handleErrorResponse(response);
305305
}
306+
306307
if (iterableApiRequest.legacyCallback != null) {
307308
iterableApiRequest.legacyCallback.execute(response.responseBody);
308309
}
309310
super.onPostExecute(response);
310311
}
311312

313+
private boolean shouldRetry(IterableApiResponse response) {
314+
return !response.success && response.responseCode >= 500 && retryCount <= MAX_RETRY_COUNT;
315+
}
316+
317+
private void retryRequestWithDelay() {
318+
final IterableRequestTask requestTask = new IterableRequestTask();
319+
requestTask.setRetryCount(retryCount + 1);
320+
321+
long delay = (retryCount > 2) ? RETRY_DELAY_MS * retryCount : 0;
322+
323+
handler.postDelayed(new Runnable() {
324+
@Override
325+
public void run() {
326+
requestTask.execute(iterableApiRequest);
327+
}
328+
}, delay);
329+
}
330+
331+
private void handleSuccessResponse(IterableApiResponse response) {
332+
IterableApi.getInstance().getAuthManager().resetFailedAuth();
333+
if (iterableApiRequest.successCallback != null) {
334+
iterableApiRequest.successCallback.onSuccess(response.responseJson);
335+
}
336+
}
337+
338+
private void handleErrorResponse(IterableApiResponse response) {
339+
if (matchesErrorCode(response.responseJson, ERROR_CODE_INVALID_JWT_PAYLOAD) && shouldRetryWhileJwtInvalid) {
340+
requestNewAuthTokenAndRetry(response);
341+
}
342+
343+
if (iterableApiRequest.failureCallback != null) {
344+
iterableApiRequest.failureCallback.onFailure(response.errorMessage, response.responseJson);
345+
}
346+
}
347+
348+
private void requestNewAuthTokenAndRetry(IterableApiResponse response) {
349+
IterableApi.getInstance().getAuthManager().requestNewAuthToken(false, data -> {
350+
try {
351+
String newAuthToken = data.getString("newAuthToken");
352+
retryRequestWithNewAuthToken(newAuthToken);
353+
} catch (JSONException e) {
354+
e.printStackTrace();
355+
}
356+
});
357+
}
358+
312359
protected void setRetryCount(int count) {
313360
retryCount = count;
314361
}
315-
316362
}
317363

318364
/**

0 commit comments

Comments
 (0)