From 3c3ad624d34ac781d58f21b37971920596b61075 Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Thu, 26 Sep 2024 11:33:35 +0100 Subject: [PATCH] VIT-7518: Exceptions thrown from OkHttp interceptor must be okio.IOException to be deemed recoverable (#166) --- .../client/utils/ApiKeyInterceptor.kt | 27 +++++++++++++++---- .../records/RecordProcessor.kt | 1 + .../io/tryvital/sample/AppSettingsStore.kt | 3 ++- .../sample/ui/settings/SettingsScreen.kt | 2 +- .../sample/ui/settings/SettingsViewModel.kt | 8 +++--- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/VitalClient/src/main/java/io/tryvital/client/utils/ApiKeyInterceptor.kt b/VitalClient/src/main/java/io/tryvital/client/utils/ApiKeyInterceptor.kt index 12fb5e28..28e37216 100644 --- a/VitalClient/src/main/java/io/tryvital/client/utils/ApiKeyInterceptor.kt +++ b/VitalClient/src/main/java/io/tryvital/client/utils/ApiKeyInterceptor.kt @@ -7,13 +7,16 @@ import io.tryvital.client.jwt.AbstractVitalJWTAuth import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Response +import okio.IOException + +data class VitalRequestError(val wrapped: Throwable): IOException() internal class ApiKeyInterceptor( private val configurationReader: ConfigurationReader, private val jwtAuth: AbstractVitalJWTAuth, ): Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val authStrategy = configurationReader.authStrategy ?: throw VitalClientUnconfigured() + val authStrategy = configurationReader.authStrategy ?: throw VitalRequestError(VitalClientUnconfigured()) val request = when (authStrategy) { is VitalClientAuthStrategy.APIKey -> { @@ -22,7 +25,14 @@ internal class ApiKeyInterceptor( } is VitalClientAuthStrategy.JWT -> { - val token = runBlocking { jwtAuth.withAccessToken { it } } + val token = try { + runBlocking { jwtAuth.withAccessToken { it } } + } catch (e: Throwable) { + // Any new exception we introduced must be wrapped in VitalRequestError. + // Otherwise OkHttp will treat it as "unexpected" and crash the process. + // https://github.com/square/retrofit/issues/3505 + throw VitalRequestError(e) + } chain.request().newBuilder().addHeader("authorization", "Bearer $token").build() } } @@ -30,9 +40,16 @@ internal class ApiKeyInterceptor( val response = chain.proceed(request) return if (response.code == 401 && authStrategy is VitalClientAuthStrategy.JWT) { - val token = runBlocking { - jwtAuth.refreshToken() - jwtAuth.withAccessToken { it } + val token = try { + runBlocking { + jwtAuth.refreshToken() + jwtAuth.withAccessToken { it } + } + } catch (e: Throwable) { + // Any new exception we introduced must be wrapped in VitalRequestError. + // Otherwise OkHttp will treat it as "unexpected" and crash the process. + // https://github.com/square/retrofit/issues/3505 + throw VitalRequestError(e) } val retryRequest = chain.request().newBuilder().addHeader("authorization", "Bearer $token").build() chain.proceed(retryRequest) diff --git a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt index f52592be..c65e67a5 100644 --- a/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt +++ b/VitalHealthConnect/src/main/java/io/tryvital/vitalhealthconnect/records/RecordProcessor.kt @@ -14,6 +14,7 @@ import androidx.health.connect.client.records.HeartRateRecord import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord import androidx.health.connect.client.records.HeightRecord import androidx.health.connect.client.records.HydrationRecord +import androidx.health.connect.client.records.NutritionRecord import androidx.health.connect.client.records.OxygenSaturationRecord import androidx.health.connect.client.records.Record import androidx.health.connect.client.records.RespiratoryRateRecord diff --git a/app/src/main/java/io/tryvital/sample/AppSettingsStore.kt b/app/src/main/java/io/tryvital/sample/AppSettingsStore.kt index ee757381..1a0fbe75 100644 --- a/app/src/main/java/io/tryvital/sample/AppSettingsStore.kt +++ b/app/src/main/java/io/tryvital/sample/AppSettingsStore.kt @@ -42,7 +42,7 @@ class AppSettingsStore( val status = VitalClient.status val isConfigured = VitalClient.Status.Configured in status - update { it.copy(isSDKConfigured = isConfigured) } + update { it.copy(isSDKConfigured = isConfigured, sdkUserId = VitalClient.currentUserId) } } companion object { @@ -70,6 +70,7 @@ data class AppSettings( val environment: Environment = Environment.Sandbox, val region: Region = Region.US, val userId: String = "", + val sdkUserId: String? = null, val isSDKConfigured: Boolean = false, ) diff --git a/app/src/main/java/io/tryvital/sample/ui/settings/SettingsScreen.kt b/app/src/main/java/io/tryvital/sample/ui/settings/SettingsScreen.kt index ea83f8f2..cb9a53d5 100644 --- a/app/src/main/java/io/tryvital/sample/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/io/tryvital/sample/ui/settings/SettingsScreen.kt @@ -75,7 +75,7 @@ fun SettingsScreen(store: AppSettingsStore, navController: NavHostController) { ) TextField( - state.value.sdkUserId, + state.value.sdkUserId ?: "null", onValueChange = {}, label = { Text("User ID") }, readOnly = true, diff --git a/app/src/main/java/io/tryvital/sample/ui/settings/SettingsViewModel.kt b/app/src/main/java/io/tryvital/sample/ui/settings/SettingsViewModel.kt index 9eaed6b8..101a567b 100644 --- a/app/src/main/java/io/tryvital/sample/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/io/tryvital/sample/ui/settings/SettingsViewModel.kt @@ -167,16 +167,18 @@ enum class SettingsAuthMode { data class SettingsState( val appSettings: AppSettings = AppSettings(), + val currentError: Throwable? = null, +) { + /** * The current SDK user ID. * * Does not have to match [AppSettings.userId] if new app settings have not yet applied to the * SDK. * */ - val sdkUserId: String = "", + val sdkUserId: String? + get() = appSettings.sdkUserId - val currentError: Throwable? = null, -) { val isApiKeyValid: Boolean get() = appSettings.apiKey != ""