Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/res/layout/screen_personalid_phoneno.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@

<TextView
android:textStyle="bold"
android:id="@+id/personalid_name_error"
android:id="@+id/personalid_phone_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
Expand Down
1 change: 0 additions & 1 deletion app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,6 @@
<string name="connectid_photo_upload_failure">"Failed to save photo. Please check your internet and try again. "</string>
<string name="this_is_not_me"><u>This is not me</u></string>
<string name="welcome_back_msg">Welcome back %s</string>
<string name="configuration_process_api_failed">Unable to contact our servers at this time. Please try again and report an issue if it persists.</string>
<string name="recovery_failed_title">Recovery failed</string>
<string name="recovery_failed_message">Looks like you’ve forgotten your Backup Code, we’re creating a new account for you</string>
<string name="personalid_wrong_backup_message">You entered the wrong Backup Code, please try again. You will need to create a new account after %d more incorrect attempts.</string>
Expand Down
4 changes: 2 additions & 2 deletions app/src/org/commcare/connect/PersonalIdManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,9 @@ private void getRemoteDbPassphrase(Context context, ConnectUserRecord user) {
ApiPersonalId.fetchDbPassphrase(context, user, new IApiCallback() {
@Override
public void processSuccess(int responseCode, InputStream responseData) {
try {
try (InputStream in = responseData) {
String responseAsString = new String(
StreamsUtil.inputStreamToByteArray(responseData));
StreamsUtil.inputStreamToByteArray(in));
if (responseAsString.length() > 0) {
JSONObject json = new JSONObject(responseAsString);
String key = ConnectConstants.CONNECT_KEY_DB_KEY;
Expand Down
2 changes: 1 addition & 1 deletion app/src/org/commcare/connect/network/ApiPersonalId.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<Respo
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
dismissProgressDialog(context);
// Handle network errors, etc.
handleNetworkError(t);
Expand Down
26 changes: 12 additions & 14 deletions app/src/org/commcare/connect/network/PersonalIdApiErrorHandler.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.commcare.connect.network;

import android.app.Activity;
import android.widget.Toast;

import org.commcare.dalvik.R;

Expand All @@ -27,26 +26,25 @@ public class PersonalIdApiErrorHandler {
*
* @param activity the context (usually the current Activity) used to display UI elements
* @param errorCode the specific {@link PersonalIdApiHandler.PersonalIdApiErrorCodes} to handle
* @param t the exception that was thrown, if any; can be null
*/
public static void handle(Activity activity, PersonalIdApiHandler.PersonalIdApiErrorCodes errorCode) {
public static String handle(Activity activity, PersonalIdApiHandler.PersonalIdApiErrorCodes errorCode,
Throwable t) {
switch (errorCode) {
case INVALID_RESPONSE_ERROR:
case JSON_PARSING_ERROR:
Toast.makeText(activity, activity.getString(R.string.configuration_process_api_failed),
Toast.LENGTH_LONG).show();
break;
case NETWORK_ERROR:
ConnectNetworkHelper.showNetworkError(activity);
break;
return activity.getString(R.string.recovery_network_unavailable);
case TOKEN_UNAVAILABLE_ERROR:
ConnectNetworkHelper.handleTokenUnavailableException(activity);
break;
case RATE_LIMIT_EXCEEDED_ERROR:
case FAILED_AUTH_ERROR:
case SERVER_ERROR:
return activity.getString(R.string.recovery_network_token_unavailable);
case TOKEN_DENIED_ERROR:
ConnectNetworkHelper.handleTokenDeniedException();
break;
return "";
case OLD_API_ERROR:
ConnectNetworkHelper.showOutdatedApiError(activity);
break;
return activity.getString(R.string.recovery_network_outdated);
default:
throw new RuntimeException(t);
}
Comment on lines +46 to 48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider safer default error handling.

The default case throws a RuntimeException which will crash the app. Consider returning a generic error message string instead to maintain app stability while still logging the unexpected error.

-default:
-    throw new RuntimeException(t);
+default:
+    if (t != null) {
+        Logger.exception("Unhandled API error", t);
+    }
+    return activity.getString(R.string.generic_api_error);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default:
throw new RuntimeException(t);
}
default:
if (t != null) {
Logger.exception("Unhandled API error", t);
}
return activity.getString(R.string.generic_api_error);
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/PersonalIdApiErrorHandler.java around
lines 43 to 45, the default case throws a RuntimeException which can crash the
app. Modify this to catch the unexpected error safely by logging the error and
returning a generic error message string instead, ensuring the app remains
stable without abrupt termination.

}
}
78 changes: 53 additions & 25 deletions app/src/org/commcare/connect/network/PersonalIdApiHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,70 +27,103 @@ public abstract class PersonalIdApiHandler {
public enum PersonalIdApiErrorCodes {
NETWORK_ERROR,
OLD_API_ERROR,
FORBIDDEN_ERROR,
TOKEN_UNAVAILABLE_ERROR,
TOKEN_DENIED_ERROR,
INVALID_RESPONSE_ERROR,
JSON_PARSING_ERROR;
JSON_PARSING_ERROR,
FAILED_AUTH_ERROR,
SERVER_ERROR,
RATE_LIMIT_EXCEEDED_ERROR;

public boolean shouldAllowRetry(){
return this == NETWORK_ERROR || this == TOKEN_UNAVAILABLE_ERROR || this == INVALID_RESPONSE_ERROR
|| this == JSON_PARSING_ERROR;
return this == NETWORK_ERROR || this == TOKEN_UNAVAILABLE_ERROR || this == SERVER_ERROR
|| this == RATE_LIMIT_EXCEEDED_ERROR;
}
}

private IApiCallback createCallback(PersonalIdSessionData sessionData,
PersonalIdApiResponseParser parser,
PersonalIdApiErrorCodes defaultFailureCode) {
PersonalIdApiResponseParser parser) {
return new IApiCallback() {
@Override
public void processSuccess(int responseCode, InputStream responseData) {
if (parser != null) {
try (InputStream in = responseData) {
JSONObject json = new JSONObject(new String(StreamsUtil.inputStreamToByteArray(in)));
parser.parse(json, sessionData);
} catch (IOException | JSONException e) {
} catch (JSONException e) {
Logger.exception("JSON error parsing API response", e);
onFailure(PersonalIdApiErrorCodes.JSON_PARSING_ERROR, e);
} catch (IOException e) {
Logger.exception("Error parsing API response", e);
onFailure(PersonalIdApiErrorCodes.JSON_PARSING_ERROR);
onFailure(PersonalIdApiErrorCodes.NETWORK_ERROR, e);
}
}
onSuccess(sessionData);
}

@Override
public void processFailure(int responseCode, @Nullable InputStream errorResponse) {
if(responseCode == 401) {
onFailure(PersonalIdApiErrorCodes.FAILED_AUTH_ERROR, null);
return;
}

if(responseCode == 403) {
onFailure(PersonalIdApiErrorCodes.FORBIDDEN_ERROR, null);
return;
}
Comment on lines +72 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should incorporate all critical reponse codes here -

401 - Auth failure
500 - Server Error
503/429 -> Rate Limited error / Too many requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9868847
I wasn't sure whether we also want unique error messages to be displayed to the user in these cases? For now I kept them all reporting "There was a network issue, please try again."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think we should have different messaging here on the lines of -
401 - You are not authorized to make this request
500 - There was an error processing your request.
503-429 -> Too many requests attemptes, try again after an hour.


if(responseCode == 429 || responseCode == 503) {
onFailure(PersonalIdApiErrorCodes.RATE_LIMIT_EXCEEDED_ERROR, null);
return;
}

if(responseCode == 500) {
onFailure(PersonalIdApiErrorCodes.SERVER_ERROR, null);
return;
}

StringBuilder info = new StringBuilder("Response " + responseCode);
if (errorResponse != null) {
try (InputStream in = errorResponse) {
JSONObject json = new JSONObject(new String(StreamsUtil.inputStreamToByteArray(in)));
if (json.has("error")) {
info.append(": ").append(json.optString("error"));
Toast.makeText(CommCareApplication.instance(), json.optString("error"),
Toast.LENGTH_LONG).show();
}
} catch (IOException | JSONException e) {
} catch (JSONException e) {
Logger.exception("JSON error parsing API error response", e);
onFailure(PersonalIdApiErrorCodes.JSON_PARSING_ERROR, e);
return;
} catch (IOException e) {
Logger.exception("Error parsing API error response", e);
onFailure(defaultFailureCode);
onFailure(PersonalIdApiErrorCodes.NETWORK_ERROR, e);
return;
}
Comment on lines +96 to 104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error in response handling of error body should not propogate same as error in response hadling of successful response body, think it's fine to eat these errors instead and have the onFailure be called with an "unknown error" message instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. Are you saying to create an UNKNOWN_ERROR enum with a different message, and get rid of the code that attempts to retrieve the "error" from the response?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and get rid of the code that attempts to retrieve the "error" from the response

Not really, we should have that code but any failures in that code shuld just return as "UNKNOWN ERROR" (enum should be fine) instead of JSON_PARSING_ERROR as that json parsing error is not the real reason this request failed.

}
onFailure(defaultFailureCode);
onFailure(PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR, new Exception(info.toString()));
}

@Override
public void processNetworkFailure() {
onFailure(PersonalIdApiErrorCodes.NETWORK_ERROR);
onFailure(PersonalIdApiErrorCodes.NETWORK_ERROR, null);
}

@Override
public void processTokenUnavailableError() {
onFailure(PersonalIdApiErrorCodes.TOKEN_UNAVAILABLE_ERROR);
onFailure(PersonalIdApiErrorCodes.TOKEN_UNAVAILABLE_ERROR, null);
}

@Override
public void processTokenRequestDeniedError() {
onFailure(PersonalIdApiErrorCodes.TOKEN_DENIED_ERROR);
onFailure(PersonalIdApiErrorCodes.TOKEN_DENIED_ERROR, null);
}

@Override
public void processOldApiError() {
onFailure(PersonalIdApiErrorCodes.OLD_API_ERROR);
onFailure(PersonalIdApiErrorCodes.OLD_API_ERROR, null);
}
};
}
Expand All @@ -102,40 +135,35 @@ public void makeStartConfigurationCall(Activity activity,
PersonalIdSessionData sessionData = new PersonalIdSessionData();
ApiPersonalId.startConfiguration(activity, body, integrityToken, requestHash,
createCallback(sessionData,
new StartConfigurationResponseParser(),
PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR));
new StartConfigurationResponseParser()));
}
public void validateFirebaseIdToken(Activity activity, String firebaseIdToken,PersonalIdSessionData sessionData) {
ApiPersonalId.validateFirebaseIdToken(sessionData.getToken(),activity,firebaseIdToken,
createCallback(sessionData,
null,
PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR));
null));
}

public void addOrVerifyNameCall(Activity activity, String name, PersonalIdSessionData sessionData) {
ApiPersonalId.addOrVerifyName(activity, name, sessionData.getToken(),
createCallback(sessionData,
new AddOrVerifyNameParser(),
PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR));
new AddOrVerifyNameParser()));
}

public void confirmBackupCode(Activity activity, String backupCode, PersonalIdSessionData sessionData) {
ApiPersonalId.confirmBackupCode(activity, backupCode, sessionData.getToken(),
createCallback(sessionData,
new ConfirmBackupCodeResponseParser(),
PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR));
new ConfirmBackupCodeResponseParser()));
}

public void completeProfile(Context context, String userName,
String photoAsBase64, String backupCode, String token,PersonalIdSessionData sessionData) {
ApiPersonalId.setPhotoAndCompleteProfile(context, userName, photoAsBase64, backupCode, token,
createCallback(sessionData,
new CompleteProfileResponseParser(),
PersonalIdApiErrorCodes.INVALID_RESPONSE_ERROR));
new CompleteProfileResponseParser()));
}


protected abstract void onSuccess(PersonalIdSessionData sessionData);

protected abstract void onFailure(PersonalIdApiErrorCodes errorCode);
protected abstract void onFailure(PersonalIdApiErrorCodes errorCode, Throwable t);
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ private void handleBackupCodeSubmission() {
}

private void confirmBackupCode() {
clearError();
enableContinueButton(false);
String backupCode = binding.connectBackupCodeInput.getText().toString();

Expand All @@ -187,8 +188,8 @@ protected void onSuccess(PersonalIdSessionData sessionData) {
}

@Override
protected void onFailure(PersonalIdApiErrorCodes failureCode) {
PersonalIdApiErrorHandler.handle(requireActivity(), failureCode);
protected void onFailure(PersonalIdApiErrorCodes failureCode, Throwable t) {
showError(PersonalIdApiErrorHandler.handle(requireActivity(), failureCode, t));

if (failureCode.shouldAllowRetry()) {
enableContinueButton(true);
Expand All @@ -210,6 +211,16 @@ private void handleSuccessfulRecovery() {
navigateToSuccess();
}

private void clearError() {
binding.connectBackupCodeErrorMessage.setVisibility(View.GONE);
binding.connectBackupCodeErrorMessage.setText("");
}

private void showError(String message) {
binding.connectBackupCodeErrorMessage.setVisibility(View.VISIBLE);
binding.connectBackupCodeErrorMessage.setText(message);
}

private void handleFailedBackupCodeAttempt() {
logRecoveryResult(false);
clearBackupCodeFields();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ private void enableContinueButton(boolean isEnabled) {
}

private void verifyOrAddName() {
clearError();
enableContinueButton(false);
new PersonalIdApiHandler() {
@Override
Expand All @@ -83,8 +84,8 @@ protected void onSuccess(PersonalIdSessionData sessionData) {
Navigation.findNavController(binding.getRoot()).navigate(navigateToBackupCodePage());
}
@Override
protected void onFailure(PersonalIdApiErrorCodes failureCode) {
navigateFailure(failureCode);
protected void onFailure(PersonalIdApiErrorCodes failureCode, Throwable t) {
navigateFailure(failureCode, t);
}
}.addOrVerifyNameCall(
requireActivity(),
Expand All @@ -93,11 +94,22 @@ protected void onFailure(PersonalIdApiErrorCodes failureCode) {
}


private void navigateFailure(PersonalIdApiHandler.PersonalIdApiErrorCodes failureCode) {
private void navigateFailure(PersonalIdApiHandler.PersonalIdApiErrorCodes failureCode, Throwable t) {
showError(PersonalIdApiErrorHandler.handle(requireActivity(), failureCode, t));

if (failureCode.shouldAllowRetry()) {
enableContinueButton(true);
}
PersonalIdApiErrorHandler.handle(requireActivity(), failureCode);
}

private void clearError() {
binding.personalidNameError.setVisibility(View.GONE);
binding.personalidNameError.setText("");
}

private void showError(String message) {
binding.personalidNameError.setVisibility(View.VISIBLE);
binding.personalidNameError.setText(message);
}

private NavDirections navigateToBackupCodePage() {
Expand Down
Loading