From 8a5324be2d3f167e4f8bea2dad42e42a54117314 Mon Sep 17 00:00:00 2001 From: Vansh Gandhi Date: Tue, 25 Jun 2024 10:28:35 -0700 Subject: [PATCH] Updated error handling (#392) * Add error handling to background job polling * Fix Interceptor error handling * Update CHANGELOG --- CHANGELOG.md | 5 +++ .../java/com/smileidentity/models/Models.kt | 2 +- .../networking/SmileHeaderAuthInterceptor.kt | 15 ++++++- .../main/java/com/smileidentity/util/Util.kt | 39 +++++-------------- .../sample/viewmodel/MainScreenViewModel.kt | 20 +++++++++- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86177b67..7e9cf1e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Notes +## Unreleased + +* Fixed a bug where some failed authentication requests were incorrectly handled +* Fixed a bug where errors with no code were not being handled correctly + ## 10.1.6 * Update generic errors with actual platform errors diff --git a/lib/src/main/java/com/smileidentity/models/Models.kt b/lib/src/main/java/com/smileidentity/models/Models.kt index bf02a399..eb98a01a 100644 --- a/lib/src/main/java/com/smileidentity/models/Models.kt +++ b/lib/src/main/java/com/smileidentity/models/Models.kt @@ -10,7 +10,7 @@ import com.squareup.moshi.JsonClass import java.io.Serializable import kotlinx.parcelize.Parcelize -@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate") @Parcelize class SmileIDException(val details: Details) : Exception(details.message), Parcelable { diff --git a/lib/src/main/java/com/smileidentity/networking/SmileHeaderAuthInterceptor.kt b/lib/src/main/java/com/smileidentity/networking/SmileHeaderAuthInterceptor.kt index d3419116..6b2ba3a7 100644 --- a/lib/src/main/java/com/smileidentity/networking/SmileHeaderAuthInterceptor.kt +++ b/lib/src/main/java/com/smileidentity/networking/SmileHeaderAuthInterceptor.kt @@ -5,6 +5,7 @@ import com.smileidentity.models.AuthenticationRequest import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request +import okio.IOException import timber.log.Timber annotation class SmileHeaderAuth @@ -20,7 +21,19 @@ object SmileHeaderAuthInterceptor : Interceptor { request.getCustomAnnotation(SmileHeaderAuth::class.java) ?: return chain.proceed(request) Timber.v("SmileHeaderAuthInterceptor: Interceptor called") val authResponse = runBlocking { - SmileID.api.authenticate(AuthenticationRequest(enrollment = true)) + try { + SmileID.api.authenticate(AuthenticationRequest(enrollment = true)) + } catch (e: Exception) { + // https://stackoverflow.com/a/58711127/3831060 + // OkHttp only propagates IOExceptions, so we need to catch HttpException (which can + // occur when credentials are not valid, for example) and rethrow it, so that the + // exception handlers at the application level can handle it. We add the caught + // exception as a suppressed exception to the IOException that we throw, so that + // [getExceptionHandler] can still access the original exception. + throw IOException("Error authenticating request").apply { + addSuppressed(e) + } + } } val newRequest = request.newBuilder() .header("SmileID-Partner-ID", SmileID.config.partnerId) diff --git a/lib/src/main/java/com/smileidentity/util/Util.kt b/lib/src/main/java/com/smileidentity/util/Util.kt index 349e43c3..86f30311 100644 --- a/lib/src/main/java/com/smileidentity/util/Util.kt +++ b/lib/src/main/java/com/smileidentity/util/Util.kt @@ -235,8 +235,10 @@ internal fun postProcessImage( * * @param proxy Callback to be invoked with the exception */ -fun getExceptionHandler(proxy: (Throwable) -> Unit) = CoroutineExceptionHandler { _, throwable -> - Timber.e(throwable, "Error during coroutine execution") +fun getExceptionHandler(proxy: (Throwable) -> Unit) = CoroutineExceptionHandler { _, parentThrow -> + Timber.e(parentThrow, "Error during coroutine execution") + // Check suppressed to handle cases where auth fails within the Interceptor + val throwable = parentThrow.suppressed.firstOrNull() ?: parentThrow val converted = if (throwable is HttpException) { val adapter = moshi.adapter(SmileIDException.Details::class.java) try { @@ -265,35 +267,11 @@ fun getExceptionHandler(proxy: (Throwable) -> Unit) = CoroutineExceptionHandler @Stable sealed interface StringResource { - fun resolve(context: Context): String + data class Text(val text: String) : StringResource - data class Text(val text: String) : StringResource { - override fun resolve(context: Context): String { - return text - } - } - - data class ResId(@StringRes val stringId: Int) : StringResource { - override fun resolve(context: Context): String { - return context.getString(stringId) - } - } + data class ResId(@StringRes val stringId: Int) : StringResource - data class ResIdFromSmileIDException(val exception: SmileIDException) : StringResource { - @SuppressLint("DiscouragedApi") // this way of obtaining identifiers is really slow - override fun resolve(context: Context): String { - val resourceName = "si_error_message_${exception.details.code}" - val resourceId = context.resources.getIdentifier( - /* name = */ - resourceName, - /* defType = */ - "string", - /* defPackage = */ - context.packageName, - ) - return context.getString(resourceId) - } - } + data class ResIdFromSmileIDException(val exception: SmileIDException) : StringResource @SuppressLint("DiscouragedApi") // this way of obtaining identifiers is really slow @Composable @@ -302,6 +280,9 @@ sealed interface StringResource { is ResId -> stringResource(id = stringId) is ResIdFromSmileIDException -> { val context = LocalContext.current + if (exception.details.code == null) { + return exception.details.message + } val resourceName = "si_error_message_${exception.details.code}" val resourceId = context.resources.getIdentifier( /* name = */ diff --git a/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt b/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt index bf6a388e..0402489e 100644 --- a/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt +++ b/sample/src/main/java/com/smileidentity/sample/viewmodel/MainScreenViewModel.kt @@ -31,6 +31,7 @@ import com.smileidentity.sample.model.Job import com.smileidentity.sample.model.getCurrentTimeAsHumanReadableTimestamp import com.smileidentity.sample.model.toJob import com.smileidentity.sample.repo.DataStoreRepository +import com.smileidentity.util.getExceptionHandler import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asFlow @@ -69,7 +70,14 @@ class MainScreenViewModel : ViewModel() { private var pendingJobCountJob = createPendingJobCountPoller() private var backgroundJobsPollingJob = createBackgroundJobsPoller() - private fun createBackgroundJobsPoller() = viewModelScope.launch { + private fun createBackgroundJobsPoller() = viewModelScope.launch( + getExceptionHandler { throwable -> + Timber.e(throwable, "Background job polling failed") + _uiState.update { + it.copy(snackbarMessage = "Background job polling failed: ${throwable.message}") + } + }, + ) { val authRequest = AuthenticationRequest(SmartSelfieEnrollment) val authResponse = SmileID.api.authenticate(authRequest) DataStoreRepository.getPendingJobs(SmileID.config.partnerId, !SmileID.useSandbox) @@ -95,6 +103,7 @@ class MainScreenViewModel : ViewModel() { BiometricKyc -> SmileID.api.pollBiometricKycJobStatus(request) EnhancedDocumentVerification -> SmileID.api.pollEnhancedDocumentVerificationJobStatus(request) + else -> { Timber.e("Unexpected pending job: $job") throw IllegalStateException("Unexpected pending job: $job") @@ -145,7 +154,14 @@ class MainScreenViewModel : ViewModel() { } } - private fun createPendingJobCountPoller() = viewModelScope.launch { + private fun createPendingJobCountPoller() = viewModelScope.launch( + getExceptionHandler { throwable -> + Timber.e(throwable, "Pending job count poller failed") + _uiState.update { + it.copy(snackbarMessage = "Pending job count poller failed: ${throwable.message}") + } + }, + ) { DataStoreRepository.getPendingJobs(SmileID.config.partnerId, !SmileID.useSandbox) .distinctUntilChanged() .map { it.size }